每个启用自动布局的UIView
在初始化后经过三个步骤:约束更新、布局和渲染。
这一步做的事情是基于约束计算 frame,系统自顶向下遍历视图层级,即从父视图到子视图,调用每个视图的updateConstraints()
方法。
setNeedsUpdateConstraints
会使约束失效,安排下一个 runloop 内更新约束。如果约束已经失效(被标记为需要更新),updateConstraintsIfNeeded
会在合适的时候触发updateConstraints
。
Apple 建议不要重写updateConstraints
,除非发现更改现有的约束太慢,此时需要在updateConstraints
中批量更新约束,同时要保证实现尽可能高效。
在此步骤中,每个视图的 frame 都将使用 Update 阶段中计算的值进行更新。系统自底向上遍历视图,即从子视图到父视图,依次调用layoutSubviews
。
当发生这两种情况时,需要重写layoutSubviews
:
调用setNeedsLayout
会使布局失效,向系统表示视图的布局需要重新计算。如果布局已经失效,layoutIfNeeded
会触发layoutSubviews
。它们的关系同 setNeedsUpdateConstraints
以及 updateConstraintsIfNeeded
方法的工作机制类似。
重写layoutSubviews
时需要注意:
super.layoutSubviews()
.setNeedsLayout
和setNeedsUpdateConstraints
,不然会死循环。此步骤负责将像素显示到屏幕上。默认情况下,UIView
将所有工作传递给一个它的CALayer
,它包含当前视图状态的像素位图。此步骤与是否用自动布局无关。
这里关键的方法是drawRect
。大多数情况下,我们可以组合使用系统已有的 view 和 layer 来构建UI,除非你使用OpenGL ES, Core Graphics 或者 UIKit 做自定义绘制,不然不需要重写这方法。
所有诸如背景颜色、添加子视图等这些操作都是自动绘制的。
假如重写了drawRect
,切记调用setNeedsDisplay(_:)
传入需要重绘的部分,不要直接调用drawRect
,就同setNeedsLayout
一样。
步骤一和步骤二在 UIViewController 中有对应的部分:
updateViewConstraints
viewWillLayoutSubviews
/ viewDidLayoutSubviews
.viewDidLayoutSubviews
是其中最重要的。它用来通知视图控制器它的视图已经完成了布局步骤(即它的bounds已经改变)。当layoutSubviews
完成后,在 view 的所有者视图控制器上,会触发 viewDidLayoutSubviews
调用。这里视图已经布局完它的子视图,并且它在屏幕上还不可见,所以我们应该把所有依赖于布局或者大小的代码放在 viewDidLayoutSubviews
中,而不是放在 viewDidLoad
或者 viewDidAppear
中。这是避免使用过时的布局位置数据的唯一方法。
Intrinsic content size是基于视图内容的固有大小。例如,一个UIImageView
的Intrinsic content size就是它的图像大小。
这里有两个技巧可以帮助简化布局和减少约束的数量:
intrinsicContentSize
方法,根据内容返回合适的尺寸。intrinsicContentSize
并为未知维度返回UIViewNoIntrinsicMetric
。AutoLayout 使用对齐矩形来定位视图。需要注意的是,intrinsicContentSize 指的是对齐矩形,而不是 frame。
默认情况下,视图的对齐矩形等于用alignmentRectInsets
修改过的 frame。为了更好地控制对齐矩形,也可以重写alignmentRect(forFrame:)
和frame(forAlignmentRect:)
。
我们来看看对齐矩形是如何影响视图定位的。
这里有一个带着30 points阴影的 image view。绿色和黑色阴影都属于同一张 image。
我已经叠加了红线来显示父视图的水平和垂直中心线。imageview 约束在父视图的中心,但是视图内容绿色方框,显然没有居中。
Xcode10.2开始, Interface Builder 可以显示我们自定义的对齐矩形。通过(Editor
> Canvas
> Layout Rectangles
)这个步骤可以在 Interface Builder 画布中显示对齐矩形。
也可以在运行时显示视图的对齐矩形。打开 scheme 编辑器,加一个启动参数-UIViewShowAlignmentRects YES
。
此时运行起来,视图的对齐矩形会被黄色框高亮。
可以看到自动布局将视图中的黄色对齐矩形居中。它不知道我们需要绿色方框居中。为了忽略掉阴影,我们需要一个新的对齐矩形,从底部和右侧去掉30点:
如果把图片放到了 Asset Catalog 里,我们可以直接修改对齐矩形。在 attributes inspector 栏下,如图所示部分修改:
如果使用多个倍数的图像(1x, 2x, 3x),那么需要为每个图像指定边距值。这里需要为1x增加30个像素,为2x增加60个像素,为3x增加90个像素。
那么如何通过代码修改呢?我们给UIImageView
加个扩展:
extension UIImageView { convenience init?(named name: String, top: CGFloat, left: CGFloat, bottom: CGFloat, right: CGFloat) { guard let image = UIImage(named: name) else { return nil } let insets = UIEdgeInsets(top: top, left: left, bottom: bottom, right: right) let insetImage = image.withAlignmentRectInsets(insets) self.init(image: insetImage) } } 复制代码
在控制器中使用时:
override func viewDidLoad() { super.viewDidLoad() setupImageView() } private func setupImageView() { guard let imageView = UIImageView(named: "Shadow", top: 0, left: 0, bottom: 30, right: 30) else { fatalError("Can't create image") } view.addSubview(imageView) } 复制代码
再次运行,我们就将看到绿色居中:
上面的例子中阴影是属于图片的一部分,我们还可以通过 UIKit 加阴影,这种方式不会影响对齐矩形。