iOS开发中的 Frame vs Bounds:实现可视化演示

2022-04-26  本文已影响0人  iOS丶lant

在 iOS 中,不仅是开发人员,还有一个空前流行且常见的与面试相关的话题。视图的框架边界之间的区别!尽管所有开发人员迟早都会知道这一点,但对于真正的初学者来说,这听起来可能是一样的。忽略更深入地研究这种差异,几乎总是可以保证麻烦和困惑在某个时候会出现。因此,为了每个开发人员的最大利益,尽快解决这个问题。

我在这篇文章中的目的是讨论这种差异。然而,我不会只说几句话来解释框架和边界是什么,我还将继续进行一个额外的步骤;逐步实现一个小的 iOS 应用程序,该应用程序将直观地展示这两个概念。这样一来,它们的全部内容就绝对清楚了,任何形式的混乱都会消失。

了解基本情况


首先,让我们从几个解释开始。框架和边界都在视图中描述了两件事

但是,如果它们都提供类似的信息,那么它们的真正区别在哪里呢?

好吧,区别在于参考坐标系。具体来说:

例如,假设一个视图控制器除了它自己的视图之外只包含一个视图(一个子视图)。该子视图的框架在视图控制器视图的坐标中描述了它的原点和大小,因为在这种情况下后者是容器(父)视图。容器的零原点(x=0 和 y=0)位于左上角,子视图的原点是与该点在两个轴上的距离。关于子视图的大小,即在任何给定时刻围绕子视图的虚拟矩形的宽度和高度。

当谈到边界时,原点是子视图本身的左上角,在它自己的坐标系中总是等于零(0, 0)。宽度和高度表示视图的实际大小,并且始终保持不变,无论可能已应用于视图的任何转换。稍后再详细介绍。

接下来我们将把所有这些都说清楚。在我们到达那里之前,下图说明了父视图和子视图的原点,如上所述:

注意:原点在 iOS 中是左上角,但在 macOS 中不是这样;原点是左下角。

基于以上所有,我认为很明显原点值是帧和边界之间的第一个差异。但这不是唯一的。尺寸也可以不同!

默认情况下,视图的大小在 frame 和 bounds 中是相同的。但是,如果我们以某种方式转换视图,则此声明将不再有效;旋转、平移(移动)和缩放,它们都会影响框架;原点和大小!

这通常是真正的问题开始的地方。如果存在基于一个或多个视图的框架的 UI 相关计算,并且这些视图中的任何一个被转换,则计算将是错误的,因为大小已更改。当然,除非这是故意的。如果不是这样,就会出现令人尴尬的视觉结果,这反过来又会导致在试图弄清楚为什么界面不像最初应该的那样表现时令人头疼。

也就是说,是时候进行快速实现了,以澄清前面提到的框架和边界之间的差异。

实施演示


在 Xcode 和一个全新的基于 UIKit 的 iOS 应用程序中,我们将实现以下内容:

所有的 UI 实现都将以编程方式完成;没有使用情节提要。所以,让我们从每个新 UIKit 项目包含的默认视图控制器开始,我们将在其中声明我们需要的几个属性:

class ViewController: UIViewController {
    var demoView: UIView!
    var frameView: UIView!
    var boundsView: UIView!
    var frameLabel: UILabel!
    var boundsLabel: UILabel!
    var rotateButton: UIButton!
    
    ...
}

除了这些,我们还需要一个属性来保持演示视图的当前旋转(以度为单位):

var  rotationDegrees:  CGFloat  =  0

接下来,我们将在 ViewController 类中定义一些方法。他们中的大多数将配置我们刚刚在上面声明的控件。

我们将关注的第一种方法是关于演示视图的初始化和配置。正如您接下来将看到的,我们给它一个背景和一个边框颜色,以使其在屏幕上具有视觉上的可区分性和突出性。

func setupDemoView() {
    demoView  = UIView(frame: .zero)
    demoView.backgroundColor = UIColor(red: 0, green: 0, blue: 255, alpha: 0.25)
    demoView.layer.borderWidth = 1
    demoView.layer.borderColor = UIColor.blue.cgColor
    view.addSubview(demoView)
    
    // Setup auto-layout constraints.
    demoView.translatesAutoresizingMaskIntoConstraints = false
    demoView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
    demoView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
    demoView.widthAnchor.constraint(equalToConstant: 300).isActive = true
    demoView.heightAnchor.constraint(equalToConstant: 140).isActive = true
}

之后,我们将初始化和配置frameView将围绕演示视图的。这里会有两个不同之处;框架视图将只有一个彩色边框,最重要的是,我们不会对其设置任何布局约束!相反,每次演示视图的框架发生变化时,我们都会动态设置它的框架:

func setupFrameView() {
   frameView = UIView(frame: .zero)
   frameView.layer.borderWidth = 2
   frameView.layer.borderColor = UIColor.magenta.cgColor
   self.view.addSubview(frameView)
}

非常相似,我们将初始化和配置boundsView. 我们使用此视图的目标是直观地表示演示视图的边界,我们将通过将演示视图的边界设置为 boundsView. 再一次,我们也不会为此视图设置任何自动布局约束。

func setupBoundsView() {
    boundsView = UIView(frame: .zero)
    boundsView.layer.borderWidth = 2
    boundsView.layer.borderColor = UIColor.green.cgColor
    self.view.addSubview(boundsView)
}

在完成上述所有操作之后,我们将添加一个按钮。它存在的目的很简单;每次我们点击它时,我们都会通过旋转几度来改变演示视图的转换。这种方法没有什么特别困难的,所以这里是它的实现:

func setupRotateButton() {
    rotateButton = UIButton(type: .system, primaryAction: UIAction(handler: { _ in
        self.transform()
    }))
    rotateButton.setTitle("Rotate", for: .normal)
    view.addSubview(rotateButton)
    
    // Specify auto-layout constraints.
    rotateButton.translatesAutoresizingMaskIntoConstraints = false
    rotateButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    rotateButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 200).isActive = true
    rotateButton.widthAnchor.constraint(equalToConstant: 80).isActive = true
    rotateButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
}

我们很快就会定义transform()在按钮的动作闭包中被调用的方法;暂时不用管它。

注意:如果您想了解更多带有动作闭包的按钮,请查看这篇文章

最后要配置的视图是两个标签,它们将报告演示视图的框架和边界值。它们的初始化和配置以两种不同的方法进行,但正如您接下来将看到的,还定义了另外两种方法。第一个实现两个标签之间的通用属性,另一个实现通用的自动布局约束。这样做有助于避免重复相同的代码两次:

func setupFrameLabel() {
    frameLabel = UILabel()
    setCommonProperties(toLabel: frameLabel)
    view.addSubview(frameLabel)
    
    // Specify auto-layout constraints.
    frameLabel.translatesAutoresizingMaskIntoConstraints = false
    frameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    
    // Configure common constraints.
    setCommonConstraints(toLabel: frameLabel)
}
 
 
func setupBoundsLabel() {
    boundsLabel = UILabel()
    setCommonProperties(toLabel: boundsLabel)
    view.addSubview(boundsLabel)
    
    // Specify auto-layout constraints.
    boundsLabel.translatesAutoresizingMaskIntoConstraints = false
    boundsLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    
    // Configure common constraints.
    setCommonConstraints(toLabel: boundsLabel)
}
 
 
func setCommonProperties(toLabel label: UILabel) {
    label.numberOfLines = 0
    label.lineBreakMode = .byWordWrapping
    label.font = .boldSystemFont(ofSize: 12)
}
 
 
func setCommonConstraints(toLabel label: UILabel) {
    label.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16)
        .isActive = true
    label.widthAnchor.constraint(equalToConstant: view.bounds.size.width/2)
        .isActive = true
    label.heightAnchor.constraint(equalToConstant: 120)
        .isActive = true
}

最后的接触


在这个小演示应用程序中完成了所有视图的配置之后,是时候添加另外三个缺失的方法了。我们将从两个类似的方法开始,它们将负责将演示视图的框架和边界值设置为两个标签的文本。但是,为了您自己的方便,我们也会让他们在控制台上打印这些值:

func printFrame() {
   print("-- FRAME --")
   print("X: \(demoView.frame.origin.x)")
   print("Y: \(demoView.frame.origin.y)")
   print("Width: \(demoView.frame.size.width)")
   print("Height: \(demoView.frame.size.height)")
   print("")
   
   frameLabel.text = """
               -- FRAME --
               X: \(demoView.frame.origin.x.rounded())
               Y: \(demoView.frame.origin.y.rounded())
               Width: \(demoView.frame.size.width.rounded())
               Height: \(demoView.frame.size.height.rounded())
   """
}

func printBounds() {
   print("-- Bounds --")
   print("X: \(demoView.bounds.origin.x)")
   print("Y: \(demoView.bounds.origin.y)")
   print("Width: \(demoView.bounds.size.width)")
   print("Height: \(demoView.bounds.size.height)")
   print("")
   
   boundsLabel.text = """
               -- BOUNDS --
               X: \(demoView.bounds.origin.x)
               Y: \(demoView.bounds.origin.y)
               Width: \(demoView.bounds.size.width)
               Height: \(demoView.bounds.size.height)
   """
}

接下来,我们将实现transform()我们之前第一次遇到的方法,每次点击旋转按钮时都会调用它。其中发生了三件重要的事情:

此外,我们还会调用printFrame()andprintBounds()方法来更新两个标签的内容。

这就是全部:

func transform() {
   // Rotate the demo view.
   rotationDegrees += 15
   demoView.transform = CGAffineTransform(rotationAngle: rotationDegrees * .pi / 180)
   
   // Update the frame of the frameView.
   frameView.frame = demoView.frame
   
   // Update the frame of the boundsView.
   boundsView.frame = demoView.bounds
   
   // Update the content of the two labels with
   // the new frame and bounds values.
   printFrame()
   printBounds()
}

最后,让我们在屏幕上布局所有内容,让我们显示两个标签的第一个内容;我们将在viewWillAppear(_:)方法中完成所有这些:

override func viewWillAppear(_ animated: Bool) {
   super.viewWillAppear(animated)
   setupBoundsView()
   setupFrameView()
   setupDemoView()
   setupFrameLabel()
   setupBoundsLabel()
   setupRotateButton()
   
   view.layoutIfNeeded()
   
   printFrame()
   printBounds()
}

见证框架和边界的差异


本教程的小应用程序已经准备好了,是时候尝试一下了。在真实设备或模拟器中运行它,准备好后开始点击旋转按钮旋转演示视图。

以下是您将看到的情况:

当演示视图旋转时,请注意:

屏幕底部的两个标签也说明了这一点。每次旋转时,原点以及演示视图框架的大小都会获得反映视图位置和尺寸的新值。但是,各个边界值保持不变,我们可以在右侧的第二个标签中看到!

borderView看到绿色边框颜色位于屏幕左上角,不要感到惊讶。这是我们应该预料到的,因为它的帧的原点值与demoView边界的原点匹配,它等于 (0, 0)。

尽管在这篇文章中我们只是通过旋转来更改演示视图的转换,但结果也与其他转换相似,例如缩放或平移。在所有情况下,演示视图的原始框架都会根据应用的转换进行更改。

结论

了解视图的框架和边界之间的区别确实有助于避免我们正在构建的用户界面出现问题。有时可能希望其他视图根据应用于视图框架的更改而相应更改,但通常这是不希望的效果。在访问原点或视图大小时,请考虑您刚刚在此处阅读的内容,并明智地选择是否通过框架或边界进行操作。

这里也推荐一些面试相关的内容,祝各位网友都能拿到满意offer!
GCD面试要点
block面试要点
Runtime面试要点
RunLoop面试要点
内存管理面试要点
MVC、MVVM面试要点
网络性能优化面试要点
网络编程面试要点
KVC&KVO面试要点
数据存储面试要点
混编技术面试要点
设计模式面试要点
UI面试要点

上一篇下一篇

猜你喜欢

热点阅读