AutoLayout 的三个阶段
每个启用自动布局的UIView在初始化后经过三个步骤:约束更新、布局和渲染。
约束更新 Update Constraints
这一步做的事情是基于约束计算 frame,系统自顶向下遍历视图层级,即从父视图到子视图,调用每个视图的updateConstraints()方法。
setNeedsUpdateConstraints会使约束失效,安排下一个 runloop 内更新约束。如果约束已经失效(被标记为需要更新),updateConstraintsIfNeeded会在合适的时候触发updateConstraints。
Apple 建议不要重写updateConstraints,除非发现更改现有的约束太慢,此时需要在updateConstraints中批量更新约束,同时要保证实现尽可能高效。
布局 Layout
在此步骤中,每个视图的 frame 都将使用 Update 阶段中计算的值进行更新。系统自底向上遍历视图,即从子视图到父视图,依次调用layoutSubviews。
当发生这两种情况时,需要重写layoutSubviews:
- 约束不足以描述视图的布局。
- 需要手动写代码计算 frame
调用setNeedsLayout会使布局失效,向系统表示视图的布局需要重新计算。如果布局已经失效,layoutIfNeeded会触发layoutSubviews。它们的关系同 setNeedsUpdateConstraints 以及 updateConstraintsIfNeeded 方法的工作机制类似。
重写layoutSubviews时需要注意:
super.layoutSubviews().- 不要调用
setNeedsLayout和setNeedsUpdateConstraints,不然会死循环。 - 不要修改当前层次结构之外的视图约束。
- 要小心更改当前层次结构的视图的约束。它将触发一个 Update 步骤,然后是另一个 Layout 步骤,可能会创建一个死循环。
渲染 Display
此步骤负责将像素显示到屏幕上。默认情况下,UIView将所有工作传递给一个它的CALayer,它包含当前视图状态的像素位图。此步骤与是否用自动布局无关。
这里关键的方法是drawRect。大多数情况下,我们可以组合使用系统已有的 view 和 layer 来构建UI,除非你使用OpenGL ES, Core Graphics 或者 UIKit 做自定义绘制,不然不需要重写这方法。
所有诸如背景颜色、添加子视图等这些操作都是自动绘制的。
假如重写了drawRect,切记调用setNeedsDisplay(_:)传入需要重绘的部分,不要直接调用drawRect,就同setNeedsLayout一样。
UIViewController相关
步骤一和步骤二在 UIViewController 中有对应的部分:
- Update:
updateViewConstraints - Layout:
viewWillLayoutSubviews/viewDidLayoutSubviews.
viewDidLayoutSubviews是其中最重要的。它用来通知视图控制器它的视图已经完成了布局步骤(即它的bounds已经改变)。当layoutSubviews完成后,在 view 的所有者视图控制器上,会触发 viewDidLayoutSubviews 调用。这里视图已经布局完它的子视图,并且它在屏幕上还不可见,所以我们应该把所有依赖于布局或者大小的代码放在 viewDidLayoutSubviews 中,而不是放在 viewDidLoad 或者 viewDidAppear 中。这是避免使用过时的布局位置数据的唯一方法。
技巧细节
Intrinsic Content Size
Intrinsic content size是基于视图内容的固有大小。例如,一个UIImageView的Intrinsic content size就是它的图像大小。
这里有两个技巧可以帮助简化布局和减少约束的数量:
- 为自定义视图重写
intrinsicContentSize方法,根据内容返回合适的尺寸。 - 如果一个视图只有一个维度的固有大小,你仍然应该覆盖
intrinsicContentSize并为未知维度返回UIViewNoIntrinsicMetric。
Alignment Rectangle
AutoLayout 使用对齐矩形来定位视图。需要注意的是,intrinsicContentSize 指的是对齐矩形,而不是 frame。
默认情况下,视图的对齐矩形等于用alignmentRectInsets修改过的 frame。为了更好地控制对齐矩形,也可以重写alignmentRect(forFrame:) 和frame(forAlignmentRect:)。
我们来看看对齐矩形是如何影响视图定位的。
这里有一个带着30 points阴影的 image view。绿色和黑色阴影都属于同一张 image。
我已经叠加了红线来显示父视图的水平和垂直中心线。imageview 约束在父视图的中心,但是视图内容绿色方框,显然没有居中。
调试 Alignment Rectangles
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 加阴影,这种方式不会影响对齐矩形。