官方文档:Auto Layout Guide 加上去年WWDC上的 Mysteries of Auto Layout 这两个 Session,以及星光社的戴铭的这篇总结深入剖析 Auto Layout,分析 iOS 各版本新增特性可以当做小抄使用,涵盖了 Auto Layout 的所有方面。再写东西只能写点不同的了,本文将搜集一些使用 Auto Layout 的痛点和技巧供参考。
Auto Layout 与 Frame
很多人盛赞 Auto Layout 是比 frame 更优雅的布局方案,我基本认同,不过,首先,Auto Layout 写起来一点都不优雅,一行 frame 代码使用 Auto Layout 需要四行代码,足以让很多人望而却步。所以官方不断在提升添加约束的体验,第三方库也不断地优化添加约束的语法来减轻开发者的痛苦。iOS 9 新推出的 Anchor 在语法上大幅简化了约束的编写,不足之处在于缺乏对multiplier
参数的设置,但不管如何优化,frame 一行代码 Auto Layout 还是四行代码。就我个人来讲,非常不喜欢 VFL,代码最啰嗦,写起来简直要命,不过在 Debug 要读懂 Log 就必须了解 VFL 的语法。老实说,使用 frame 时一行代码四个数字就可以确定视图的位置和大小比起有多种可能方案的 Auto Layout 布局实际上要舒心得多,虽然后者可读性更好,不过 frame 往往也是需要计算的,从成本上讲有时候两者挺接近的;Auto Layout 的真正优势在于自动化,我们只需要给系统一堆布局方程式,剩下的事情就不用我们操心了,这在分辨率适配、多视图互相约束协作等方面显得极其高效(优雅),这也是我能忍受写一堆约束的原因。
刚接触 Auto Layout 时一直想搞清楚这两者的关系,简单来讲,Auto Layout 将约束条件转化为视图的 frame。原来的布局过程直接使用 frame 指定视图的位置和大小,AutoLayout 参与布局后通过约束计算出 frame,再应用到视图上。具体过程可参考 Mysteries of Auto Layout, Part 2 的开头部分,深入剖析Auto Layout,分析iOS各版本新增特性也总结了视频中的这部分内容。
如何妥善处理这两者的关系?
Frame 自动转化为约束
既想享受 frame 的便捷,又想得到 Auto Layout 的好处,鱼与熊掌能兼得么?
UIView 的translatesAutoresizingMaskIntoConstraints
属性在这里派上用场,该属性为true
时,设置 frame 会自动转化为约束,修改 frame 时也会自动调整约束。这时候就不要再手动添加约束了,你再添加约束往往会造成冲突,注意是往往,因为此时视图上的约束已经是唯一可解的了,你添加的往往是优先级最高的约束,必然造成冲突,在控制台能看到NSAutoresizingMaskLayoutConstraint
这种类型的约束与你添加的约束无法同时满足,这里甚至有温馨提示你查看translatesAutoresizingMaskIntoConstraints
属性的文档,想必苹果也知道大家经常忘记把这个属性关闭。或许你可以添加可选约束,不过这样一来就没什么意义了。那么可以修改这种自动添加的NSAutoresizingMaskLayoutConstraint
约束吗?实际上无法找到这样的约束的,它被系统隐藏了,你只能在发生冲突时才能在控制台看见它们。
这个属性与原来的 auto resize mask 结合后能产生很好的效果,如下所示:添加了宽度和高度方向的 mask 后,当 containerView 的尺寸发生变化后,subView 也会随之变化,享受了 Auto Layout 的好处,还不用写约束。
subView.translatesAutoresizingMaskIntoConstraints = true
subView.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
containerView.addSubview(subView)
subView.frame = containerView.bounds
translatesAutoresizingMaskIntoConstraints
属性将两种的布局机制的优点结合起来,你可以在某个子视图上使用 frame,其他的子视图使用约束来布局,互不干扰。这种混合机制就好比 Objective-C 与 Swift 在同一个工程中使用,但你不能在 Objective-C 文件中使用 Swift 语言,或者在 Swift 文件中使用 Objective-C 语言,起初我还真就这么认为的。
在 storyboard 里,这个属性是默认关闭的,在代码里生成的视图的该属性默认是开启的。如果你不想用约束,又希望 AutoLayout 能帮你打理,就开启这个属性。其他情况下,应该关闭这个属性。
各自为政
如果嫌弃写约束太麻烦,也可以不使用 Auto Layout,使用 Auto Layout 的视图去折腾约束,剩下的视图就由你负责手动处理 frame 了。觉得 iOS 7 看起来好丑,我还是用我的 iOS 6 吧,没问题,只是很多新特性无法享受罢了。
直接的混血模式,危险!
正常情况下,两者的合作方式应该只有上面两种,但经常还是有人将 Auto Layout 与 frame 在一个视图上混合使用,因为看上去好像都能正常工作,但可能会遇到各种疑难杂症,原因在于两者更新布局的机制差异。
Auto Layout 对视图进行布局的唯一依据是视图的约束,约束发生变化后会触发约束机制重新计算视图的 frame 并更新,这种情况包括:约束的修改和优先级的变化,添加或移除约束,添加或移除添加了约束的视图。当然还有其他事件会触发约束变化,文档 Understanding Auto Layout 中列举了这些情况。在代码中我们造成这样的约束变化后 Auto Layout 会自动更新视图的布局,但有时候你不能期待布局会立即更新,因为 Auto Layout 要搜集约束变化,计算新的布局,然后遍历受影响的视图重新布局,在性能上比直接设置 frame 要慢一点。你可以在拥有变化的约束的视图上调用layoutIfNeeded()
强制立刻更新布局。
直接设置 frame 并不会修改视图相关的约束(除了开启translatesAutoresizingMaskIntoConstraints
),而约束的一切变化会转化为新的 frame,两者之间的影响是单方向的。同时修改视图的 frame 和约束,最终结果还是以约束转化的 frame 为准。前后脚修改 frame 和约束,这种瞬间的布局变化两者混合使用不会出错,因为最终都修改了 frame;但如果用这种方式进行动画会出现偏差,因为两者之间的影响是单方向的,必然会造成状态的不连续,这是可以预见的;在这种情况下,如果希望动画完全符合你的预期,必须保证约束与 frame 是匹配的,但这样一来,修改 frame 后还得修改约束,实在没必要,使用 Auto Layout 就老老实实地使用约束吧。
来看看例子:
@IBOutlet weak var testView: UIView!
@IBOutlet weak var centerXConstraint: NSLayoutConstraint!
var center = testView.center //假设此时 center.x 的值为160
center.x += 10
testView.center = center //现在 testView 的 center.x 为 170
centerXConstraint.constant += 10 //现在 testView 的 center.x 依然为 170,因为直接修改 frame 并不影响约束,约束只参照约束
centerXConstraint.constant += 10 //现在 testView 的 center.x 为 180
testView.center.x += 10 // 现在 testView 的 center.x 为 190,但从 Auto Layout 的角度看依然是 180
//动画从 190 变化到 200
UIView.animateWithDuration(0.5, animations: {
testView.center.x += 10
})
//尽管从 Auto Layout 的角度看 center.x 是 180,修改约束后该值为 190,这个动画应该是从 180 变化到 190,但实际动画是从当前的 200 变化到 190。
UIView.animateWithDuration(0.5, animations: {
self.centerXConstraint.constant += 10
self.testView.superView.layoutIfNeeded()
})
顺便提一下使用 Auto Layout 做动画的方法。UIView 中更新布局的相关方法:
Laying out SubViews
- layoutSubviews() //不要直接调用,如果需要强制更新布局,调用下面的 setNeedsLayout()
- setNeedsLayout() //标记布局需要在下一个周期更新
- layoutIfNeeded() //立刻更新布局
Triggering Auto Layout
- setNeedsUpdateConstraints() //标记约束需要在稍后更新,系统会调用下面的 updateConstraints()方法,修改多个约束后调用该方法批量更新有助于提升性能
- updateConstraints() //更新调用该方法的视图的约束
- updateConstraintsIfNeeded() //更新调用该方法的视图以及其子视图的约束
使用 Auto Layout 时提交动画与普通的动画没有什么区别,在 Block 里修改相关属性,只不过最后需要调用layoutIfNeeded()
立即更新布局,其他方法无效。在网上的一些例子里,也有将修改的步骤放在 Block 之外,起初我搜索到的就是这样的方式,为了强制保持一致的代码风格,我现在回到了下面的风格。
UIView.animateWithDuration(0.5, animations: {
....../*修改 view 的约束*/
view.superView.layoutIfNeeded() //立刻更新其下的子视图的布局
})
很多时候我们喜欢在viewDidLoad()
做一些设置,但此时还没有开始布局视图,如果你希望在此修改约束,记得调用layoutIfNeeded()
方法立刻更新布局使得修改生效。
约束(contraint)的拥有者
我一直觉得使用 AutoLayout 的另外一个重大障碍是找出需要的约束。在 IB 中添加的约束可以在实现文件里用 IBOutlet 来引用,但可能不会引用每一条约束,或者你是在代码里添加的呢,所以第一个问题是,怎么找出要修改的约束?约束往往涉及两个视图,是否在这两个视图上都保存了一份呢,要修改是否需要修改两份?否。约束保存在两个视图最近的父类视图中或者两者中层级比较高的那个视图,事实上你如果将约束添加到两者中层级较低的那个视图会出现错误。这是由自动布局的机制决定的,布局更新的顺序是从上到下,从外到内,在更新布局时需要根据视图上的约束对其下的子视图进行布局,添加到子视图上显然不利于布局的计算。从另外一个角度讲,视图只会保存它的子视图相关的约束,以及参与对象中较高层次是自身的约束,比如设定self.height = self.width * 0.5
这种约束。
知道了地方还需要找到指定的约束,从下面方法的参数可以看到,如果希望直接对比视图来查找往往需要一番转换才行,而且还需要对比两个参数 view1 和 view2,这实在是很不方便。
init(item view1: AnyObject, attribute attr1: NSLayoutAttribute, relatedBy relation: NSLayoutRelation, toItem view2: AnyObject?, attribute attr2: NSLayoutAttribute, multiplier multiplier: CGFloat, constant c: CGFloat)
约束还有一个属性var identifier: String?
用于标记,这在 Debug 时面对超长 Log 时非常省心,这里设定该值用于查找能够省点力气。相比使用 frame 时可以一步修改时的便捷,寻找约束的过程可能就耗尽了你对 AutoLayout 的向往。
另外,从 iOS 8 开始添加约束不必再用view.addConstraint(constraint)
这种方法了,如前面所说,这种方法必须将约束添加到最近的父视图或是参与约束中层级较高的那个视图中,在 iOS 8 里,NSLayoutConstraint
类添加的var active: Bool
属性可以自动调用相关视图的addConstraint:
和removeConstraint:
方法,不必需要我们操心约束的正确拥有者了。新生成的约束该属性为false
。
另外 iOS 8 还添加了以下两个批量处理的方法:
class func activateConstraints(_ constraints: [NSLayoutConstraint])
class func deactivateConstraints(_ constraints: [NSLayoutConstraint])
优先级 Priority
我刚开始接触 AutoLayout 的时候知道约束可以是不等式,只要所有的约束有解且唯一就可以保证布局正常,但不知约束的优先级有何用处。约束的优先级值的范围为半开区间(0,1000]之间的 Float,优先级值为1000时表示该约束必须满足,其他的值表示该约束是可选的。在 storyboard 里可以看到优先级约定了这么几个常量:
Required 1000
Hight 750
Low 250
优先级值为1000时该值不可再变化,可选约束的优先级值可以在(0,1000)之间随意变化,不能为1000,必须满足的约束和可选的约束一旦生成了就不能转化到对方的阵营里去。如果所有的 Required 约束不能唯一确定布局,就会从优先级次高的约束中补充,依然不能唯一确定布局的话,再从次级的约束中补充。所以不妨换一种思维方式,不需要所有的约束都是 Required 级别的,只要保证优先级靠前的约束能唯一确定布局就可以了。
由于优先级在1000以下的约束都是可选的,可以针对不同的布局需求添加多个可选约束而且不会引起约束冲突,通过调整这些可选约束的优先级可以实现不同的布局。
上面的例子来自这篇文章 Animating Autolayout Constraints,这篇文章的最后一次修改中通过分别修改了两个约束的 constant 和优先级来实现这个动画。为了让这个示例更典型一点,这里也稍微修改一下原文的实现,仅仅修改其中一个约束的优先级就可以实现这个动画(黄蓝视图的间距会稍有不同)。实现方法:添加如下的的约束条件,只保证两个视图之间的距离约束是必须实现的,相对于尾部的距离约束都是可选的,哪个优先级高就满足哪个。这里的两个可选约束优先级一样,是无法确定唯一布局的,必须打破这个局面。
yellowView.Trailing = 1.0 * superView.TrailingMargin (priority = 750)
blueView.Trailing = 1.0 * superView.TrailingMargin (priority = 750)
blueView.Leading = 1.0 * yellowView.Trailing + 18(priority = 1000)//视图间的标准间距为8,这里为了将 blueView 挤出屏幕,多加了10个单位
另外还有个前提约束:yellowView.width = 1.0 * blueView.width (priority = 1000)
在 Switch 的响应方法中更改其中一个尾部约束的优先级值,此时满足上面三个约束中优先级最高的两个约束就可以唯一确定布局,这样当两个可选布局的优先值的排位不一样时就形成了两种布局:
func updateConstraintsForMode {
if (self.modeSwitch.isOn) {
self.blueViewTrailingConstraint.priority = UILayoutPriorityDefaultHigh + 1
} else {
self.blueViewTrailingConstraint.priority = UILayoutPriorityDefaultHigh - 1
}
}
如果通过更改约束本身的构成条件来完成上面的效果,最简单的办法是去掉 yellowView 与父视图的尾部距离约束,然后修改blueViewTrailingConstraint
的常量值。这种方法可以说是非常的 frame 风格,也是很容易想到的。孰优孰劣不好说,这个例子就算是给你提供另外一种可能吧。
优先级在一些拥有固有尺寸(intrinsicContentSize)的视图上运用得比较多,像 UILabel,UIButton,UITextField 这类为了优先保证内容显示完整的控件,在 storyboard 里添加约束时仅仅需要添加两个位置相关的约束就可以了。 在这里可以看到相关的讨论:Priority: Content hugging vs Content compression resistance。
约束系数 Multiplier
约束的组成:
前面说过 AutoLayout 的真正优势是自动化。常见的场景是,比如多个子视图的 centerX 相对于父视图的 centerX 保持一定的距离,该距离与子视图在队列中的位置有关,其中一个视图在该方向的位置发生变化时,后面的视图会自动更新位置,使用 frame 是难以如此便捷地做到的。但有时候考虑到某个子视图可能会移除,这样需要重新配置约束,觉得麻烦,怎么办,回到 frame 的方法,每个子视图单独与父视图配置约束,这样不影响其他子视图。当屏幕旋转时如果父视图的宽度发生了变化,如何自动维持这个规律?我刚开始使用 AutoLayout 时还不适应这种布局方式,仅仅只是将 frame 翻译为对应的约束,但采用的手法却是将对应的距离转化为 constant,比如下面这种:
subView.centerX = 1.0 X containerView.centerX + (i * containerView.frame.width)
然而当父视图宽度变化时却发现子视图并没有自动更新位置。问题在哪?搞错了这个约束方程式的变量,constant 设定后它就不会变了,应该在父视图的 centerX 这个变量上做文章。我默默地将multiplier
设置为1后的约束尽管也发生了变化(很微量往往察觉不到),但没有达到预期,应该这么做:
subView.centerX = (2 * i + 1) X containerView.centerX + 0
这样每一个子视图都会随着父视图的 centerX 的变化自动更新自己的位置。具体例子可以参考这里。
当你需要设定一些特别的比例时,比如 3/7, 7/13 之类,在代码中可以很方便地就这样原封不动地交给算式表达式来计算,在 storyboar 里怎么弄呢,3:7, 7:13 这样就可以了。