分析iOS各版本新增特性,自动布局

作者:新葡京简介

先前写到的一篇Masonry心得文章里已经提到了很多AutoLayout相关的知识,这篇我会更加详细的对其知识要点进行分析和整理。

目录

先抛出结论:

我们先看一个 Layout 的周期

一般大家都会认为Auto Layout这个东西是苹果自己搞出来的,其实不然,早在1997年Alan Borning, Kim Marriott, Peter Stuckey等人就发布了《Solving Linear Arithmetic Constraints for User Interface Applications》论文(论文地址: constraint-solving算法实现,并且将代码发布在他们搭建的Cassowary网站上 Layout。

0、前言

setNeedsUpdateConstraints 保证之后肯定会调用 updateConstraintsIfNeeded .

新葡京8455 1

Cassowary是个解析工具包,能够有效解析线性等式系统和线性不等式系统,用户的界面中总是会出现不等关系和相等关系,Cassowary开发了一种规则系统可以通过约束来描述视图间关系。约束就是规则,能够表示出一个视图相对于另一个视图的位置。

一、Auto Layout前世今生

SetNeedsLayout 保证之后肯定会调用 layoutIfNeeded .

项目以 Run Loop 的形式启动,当约束发生改变的时候,Layout Engine 会重新计算 Layout,之后到 Deferred Layout Pass, 最后,所有的布局完成
约束的改变包括了下面几个方面:

进入下面主题前可以先介绍下加入Auto Layout的生命周期。在得到自己的layout之前Layout Engine会将Views,约束,Priorities,instrinsicContentSize(主要是UILabel,UIImageView等)通过计算转换成最终的效果。在Layout Engine里会有约束变化到Deferred Layout Pass再到应用Run Loop再回到约束变化这样的循环机制。

二、Auto Layout基础知识
  • 1.Auto Layout本质
  • 2.Auto Layout基本原理
    NSLayoutAttribute布局属性
    约束关系
    约束优先级
    约束的安装与移除

AutoLayout 的本质

AutoLayout 是指,用一套规则(约束)来定义视图之间的位置。

AutoLayout 能够让每个 view 有唯一的 frame。

其实,这样子解释,还是让人很难理解,所以接下来会简单介绍下 AutoLayout ,在对其有所了解和深入后,再解释下面这几个问题:

  • 保证之后调用 的之后是在什么时候?
  • 这些方法的调用时序大概是怎么样的?
  • 为什么要先 set 一下,而不是直接 updateConstraint 和 layout
  • 约束的 Activating 或 Deactivating
  • 改变 constant 或者 改变 priority
  • 添加或者删除视图
    当收到约束改变的通知以后,Engine 做的第一件事情就是重新计算 Layout,这里我猜想是计算包括优先级在内以及 instrinsicContentSize 的有效的约束,以及排除一些无效的约束,当 Layout Engine 收到新的值以后,会调用 superView 的 setNeedsLayout 方法,通知 superView 重新布局。这也就是为什么会导致延迟布局的原因。

触发约束变化包括

三、Auto Layout多种使用方式
  • 1.NSLayoutConstraint
    NSLayoutConstraint对象创建约束
    NSLayoutConstraint优缺点讨论
    示例代码
  • 2.VFL
    VFL创建约束
    VFL优缺点讨论
    示例代码
  • 3.Interface Builder
  • 4.NSLayoutAnchor
    NSLayoutAnchor创建约束
    NSLayoutAnchor优缺点讨论
    示例代码
  • 5.Masonry
    示例代码
  • 6.UIStackView
    UIStackView布局方案特点
    UIStackView特殊性
    示例代码

UIView 生命周期

init=>start: InitWithFrame

layout=>operation: setNeedsDisplay: 
setNeedsUpdateConstraint:  
setNeedsDisplay:

ifUpdateCons=>condition: update constraints ?
updateCons=>operation: updateConstraints
ifLayout=>condition: update layout ?
layoutSubview=>operation: layoutSubviews
ifDisplay=>condition: needs display ?
draw=>operation: Draw in Rect
e=>end: Event Loop

init->layout->ifUpdateCons
ifUpdateCons(yes)->updateCons->ifLayout
ifUpdateCons(no)->ifLayout
ifLayout(yes)->layoutSubview->ifDisplay
ifLayout(no)->ifDisplay
ifDisplay(yes)->draw(right)->e(right)
ifDisplay(no)->e(right)
e->ifUpdateCons

在 iOS 中,AutoLayout Engine 是一个迭代机。

为什么这样说呢?先回到手写布局时代,我们通过计算 view 与屏幕之间的相对距离,得出 view 实际的 frame,然后赋值给 view,这样子每个 view 都会按照我们设置的位置正确地显示。

接着我们来到当下的 iOS 开发,先列出几个问题:

  • 需要适配多种屏幕
  • 既可以在 iPhone 上使用,又可以在 iPad 上使用
  • 可以在横屏下使用
  • 在不同的屏幕尺寸下,有不同的布局方式(比如屏幕小,就一行放一个,屏幕大一行放两个)

如果仍然在原始的手写布局下去完成上述工作,势必累死,也不一定能够很好的完成。


  • Activating或Deactivating
  • 设置constant或priority
  • 添加和删除视图
四、Auto Layout关键知识
  • 1.Auto Layout布局原则
    坚持一致的布局方式
    创建充分的、可满足的约束
  • 2.translatesAutoresizingMaskIntoConstraints
  • 3.alignmentRect对齐矩形
    alignmentRect简介
    alignmentRect应用场景
    alignmentRect可视化
  • 4.内在内容大小IntrinsicContentSize
    IntrinsicContentSize介绍
    IntrinsicContentSize原理分析
    IntrinsicContentSize应用
    内在内容尺寸IntrinsicContentSize与自适应尺寸FittingSize
  • 5.Auto Layout与国际化
    布局方向适配
    布局测试

搞个机器人

人类能够发展到现在的文明,就是因为 善假于物也

我希望现在有一套东西,我只需要告诉它,我希望视图表现成什么样子,然后它就会按照我的期望去计算出每个 view 的 frame,我只需要把 frame 拿来用就行了。

AutoLayout 就是这样的一套东西,它接收视图与视图之间的规则,生成最终的 frame。

这里有个误区,AutoLayout 的确是最终生成了 frame,不过生成之后自动给 view 赋值上去了,所以我们没有看到 setFrame 这个过程。

这个规则就是约束 constraint。

最后一步是 Deferred Layout Pass, 这一步过后,所有 view 的 frame 将被重新布局完毕,
Deferred Layout Pass 包含了 2 个步骤

这个Engine遇到约束变化会重新计算layout,获取新值后会call它的superview.setNeedsLayout()

五、Auto Layout布局周期
  • 1.Auto Layout布局机制
  • 2.Auto Layout布局流程
    Constraints Change
    Deferred Layout Pass
  • 3.Auto Layout布局流程总结

为什么需要 update constraint

在 UIView 显示之前,先判断 view 是否能根据当前约束计算出唯一的 Frame。如果可以,那么就根据这个 Frame 去布局。同时,在这里我们认为 view 是满足约束的。

严谨一些,是指在 AutoLayout Engine 中,该 view 的 constraint 是否为最新。

AutoLayout Engine 是一个单独的约束处理系统,在绝大多数操作中,比如:

  • Activating或Deactivating 启用和停用
  • 设置constant或priority
  • 添加和删除视图

AutoLayout Engine 本身都会标记 view 的约束不是最新,即调用 setNeedsUpdateConstraint

但是也有例外,AutoLayout Engine 并不知道你又修改了约束。因此在这种 case 下,需要手动调用 setNeedsUpdateConstraint 来标记约束需要更新。

  • 对 constraints 的错误处理 (比如 你让一个 view 居中,但是没有设定它的宽度和高度,Deferred Layout Pass 会处理这些事情)
  • 重新布局 view 的位置
    完成 Layout 的布局以后,将 subview 的 frame 从 Layout Engine 中拷贝出来给视图,并且从父类开始,向下调用 layoutSubview() 方法
    WWDC 中还强调,最好不要去重写 layoutSubviews(), 如果你真的要这么做,那么你需要注意这么几件事情:
  • 当你的约束不足的时候,去重写 layoutSubviews, 补充不足的约束(比如 补充一些 没有使用 AutoLayout 的 subview 的 frame)
  • 一些 view 已经 layout 完毕了,还有一些 view 没有布局完成(应该是指和自己是兄弟 view 的视图),不过它们马上就会被 layout。

在这个时候主要是做些容错处理,更新约束有些没有确定或者缺失布局声明的视图会在这里处理。接着从上而下调用layoutSubviews()来确定视图各个子视图的位置,这个过程实际上就是将subview的frame从layout engine里拷贝出来。这里要注意重写layoutSubviews()或者执行类似layoutIfNeeded这样可能会立刻唤起layoutSubviews()的方法,如果要这样做需要注意手动处理的这个地方自己的子视图布局的树状关系是否合理。

六、References

setNeedsUpdateConstraint

setNeedsUpdateConstraint 控制 view 的约束是否需要更新。当一个自定义view的某个属性发生改变,并且可能影响到constraint时,需要调用此方法去标记constraints需要在未来的某个点更新,系统然后会调用 updateConstraints,. 以解决这个由属性改变带来的影响。

Do

  • 调用 super.layoutSubviews()
  • 所有的操作应该只在你这个 view 的 subtree 中
  • 不要期望frame会立刻变化。
  • 在重写layoutSubviews()时需要非常小心。

0、前言

  • 日常开发中,UI搭建、调试会占用我们大部分的时间,以至于移动端开发经常会被调侃为搭界面的。提高UI布局技术可以提高开发效率,把更多的时间放在优化、逻辑方面,而不是被界面业务绑死。本文是笔者近期学习的相关布局基础,没有很高深的技术,都是讲述原理性、基础性的东西。示例代码详见DEMO,欢迎留言或者邮件(mailtolinbing@163.com)勘误、交流。

updateConstraintsIfNeeded

updateConstraintsIfNeeded立即触发约束更新,自动更新布局。

Don't

  • 不要调用 setNeedUpdateConstraints()
  • 不要去操作不在这个 view 的 subtree 中的其它视图
  • 不要去盲目的在这里修改 constraint (我猜就是不要修改 constraint 的意思)
    整个 layout cycle 需要注意的两个事项:
  • 不要期望 frame 会立刻改变
  • 重写 layoutSubviews() 要小心

Auto Layout你的视图层级里所有视图通过放置在它们里面的约束来动态计算的它们的大小和位置。一般控件需要四个约束决定位置大小,如果定义了intrinsicContentSize的比如UILabel只需要两个约束即可。

一、Auto Layout前世今生

  • Auto Layout是苹果公司在iOS6发布的界面布局技术,并随着iOS SDK的迭代逐步完善了各种布局API、提供多种使用Auto Layout的布局方式。实际上Auto Layout算法本身并非有Apple发明,Auto Layout源于Cassary约束解析工具包。该算法由Alan Borning、Kim Marriott、Peter Stuckey、Yi Xiao于1997年发布,而后被多门流行编程语言采用,Objective-C是其中之一。该算法的主要思想是:将基于约束系统的布局规则(本质上是表示视图布局关系的线性方程组)转化为表示规则的视图几何参数。

updateConstraints

当 Custom View 发现属性或者其他的改变导致它的所有约束中有一个失效时,首先应该删除这个失效的约束,然后调用 setNeedsUpdateConstraints 表示当前的约束需要更新,然后在 updateConstraints 中恰当地地方检查当前 content 所需的必要约束。

注意:要在实现在最后调用[super updateConstraints]

view1.attribute1 = mutiplier * view2.attribute2 constant

二、Auto Layout基础知识

layoutSubviews

在确定了 view 的约束后,AutoLayout 通过计算可以得出 view 的 frame,计算的过程就是 layout 的过程。

Layout 的顺序,是由最外层向里递进,所以子视图只需要相对于父视图做好布局就可以。

在调用 layoutSubviews 的同时,也会调用 setNeedsUpdateConstraint。

redButton.left = 1.0 * yellowLabel.right   10.0 //红色按钮的左侧距离黄色label有10个point

1.Auto Layout本质

  • Auto Layout本质就是一个线性方程解析Engine。基于Auto Layout的布局,不在需要像frame时代一样,关注视图尺寸、位置的常数,转而关注视图之间关系,描述一个表示视图间布局关系的约束集合,由Engine解析出最终数值。
  • 一个约束对象NSLayoutConstraint,本质上是表示两个视图之间(当表示尺寸时只表示视图本身)布局关系的一个线性方程,该方程可以是线性等式、也可以是线性不等式。
  • 多个约束对象组成是一个约束集合,本质上是表示某个界面上多个视图之间布局关系的线性方程组。方程组中的多个线性方程,以数字标识的优先级进行排序(UILayoutPriority,本质上是浮点型float)。
  • Auto Layout Engine根据按照线性方程的优先级从高到底对线性方程组进行解析,求得方程组的解。
    • 当设置的约束欠缺,即存在约束歧义,线性方程组有多个解,而不是唯一解。这便是约束错误的一种:约束不充分,可能导致视图丢失,视图错位。
    • 当设置的约束过多,存在多个优先级相同的描述同一个关系的线性方程,并且约束产生的效果不同(例如 View1.left = View2.right 10 ; View1.left = View2.right 20,优先级都为1000),线程方程组无解。这是约束错误的另一种:约束不可满足,产生约束约束冲突,控制台会Log错误日志,同样可能造成布局错误。

Auto Layout Process 自动布局过程(引用自Objccn.io)

与使用springs and struts(autoresizingMask)比较,Auto layout在view显示之前,多引入了两个步骤:updating constraints 和laying out views。

每一个步骤都依赖于上一个。display依赖layout,而layout依赖updating constraints。显示之前首先得知道布局,想要完整的布局就得更新约束(约束才能得出布局啊)。

updating constraints->layout->display

第一步:updating constraints,被称为测量阶段,其从下向上(from subview to super view),为下一步layout准备信息。

可以通过调用方法setNeedUpdateConstraints去触发此步。constraints的改变也会自动的触发此步。但是,当你自定义view的时候,如果一些改变可能会影响到布局的时候,通常需要自己去通知Auto layout,updateConstraintsIfNeeded。

自定义view的话,通常可以重写updateConstraints方法,在其中可以添加view需要的局部的contraints。

第二步:layout,其从上向下(from super view to subview),此步主要应用上一步的信息去设置view的center和bounds。可以通过调用setNeedsLayout去触发此步骤,此方法不会立即应用layout。如果想要系统立即的更新layout,可以调用layoutIfNeeded。另外,自定义view可以重写方法layoutSubViews来在layout的工程中得到更多的定制化效果。

第三步:display,此步时把 view 渲染到屏幕上,它与你是否使用Auto layout无关,其操作是从上向下(from super view to subview),通过调用setNeedsDisplay触发,

因为每一步都依赖前一步,因此一个display可能会触发layout,当有任何layout没有被处理的时候,同理,layout可能会触发updating constraints,当constraint system更新改变的时候。

需要注意的是,这三步不是单向的,constraint-based layout是一个迭代的过程,layout过程中,可能去改变constraints,有一次触发updating constraints,进行一轮layout过程。

注意:如果你每一次调用自定义layoutSubviews都会导致另一个布局传递,那么你将会陷入一个无限循环中。

就是说,layout 和 updateConstraints 不断迭代最终确立了整个布局和显示,然后交给屏幕去显示

使用NSLayoutConstraint类添加约束。NSLayoutConstraint官方参考:

2.Auto Layout基本原理

新葡京8455 2

图片出处 :苹果官方文档 - Auto Layout Guide

  • 正如上文提及,一个约束本质上就是一个表示视图布局关系的线性方程。一个完整的约束方程式如图所示。图示表示的布局关系是:RedView的左边距离BlueView的右边8个点。
    • Item1、Item2:一般是UIView,表示该约束关系对应的两个视图,当约束等式表示尺寸时,其中一个Item为nil。
    • Attribute1、Attribute2:NSLayoutAttribute类型,表示约束属性。当约束等式表示尺寸时,其中一个Attribute为NSLayoutAttributeNotAnAttribute,表示占位,无任何意义。
    • Relationship:NSLayoutRelation类型,表示约束关系,可以是=、>=、<=。
    • Multiplier:CGFloat类型,表示倍数关系,一般用于尺寸(eg:Item1的宽度为Item2的两倍,则Multiplier为2.0)
    • Constant:CGFloat类型,表示常数。

实践出真知

[NSLayoutContraint constraintWithItem:view1 attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:view2 attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-5]
NSLayoutAttribute布局属性
布局属性 表示意义
NSLayoutAttributeWidth、NSLayoutAttributeHeight 表示视图的尺寸:宽、高
NSLayoutAttributeLeft、NSLayoutAttributeRight 表示视图的X轴方向的位置:左、右
NSLayoutAttributeLeading、NSLayoutAttributeTrailing 表示视图的X轴方向的位置:前、后
NSLayoutAttributeTop、 NSLayoutAttributeBottom 表示视图Y轴方向的位置:顶、底
NSLayoutAttributeBaseline 表示视图Y轴方向的位置:底部基准线
NSLayoutAttributeCenterX、NSLayoutAttributeCenterY 表示视图的中心点:视图在X轴的中心点、视图在Y轴的中心点

注意点

  • 只有同类型的约束才能互相做约束
    表示尺寸的约束width/height只能与其他视图的width/height做约束,或者与非负常数做约束;
    表示Y轴方向的约束属性(top、bottom、baseLine、CenterY)只能与Y轴方向的约束属性做约束;
    表示X轴方向的约束属性只能与表示X轴的约束属性做约束,且leading/trailing不可以跟left/right做约束。

  • leading表示前边、trailing表示后边,在阅读习惯从左到右的语言中,leading相当于left、trailing相当于right。在从右到左的语言中,leading相当于right、trailing相当于left。

  • baseLine指视图的文本内容底部,该属性只对有文本的控件类型有效(UILabel、UIButton…),并且只有当控件赋值了文本,该约束才能正确布局。文本控件的文字顶部与底部与控件本身会有间隙,当要实现文本底部对齐,可使用该约束属性。

  • Item1、Item2位置问题

    • 从数学的角度,线性方程式两边的Item1、Item2是可以调换位置的。eg:View右边距离父视图superView右边10pt,可以表示为View.right = superView.right - 10 ; 也可以表示为superView.right = View.right 10。可以保持视图顺序,使用负数;也可以保持数值为正数,调换视图顺序。

layoutIfNeeded 调用导致 Crash

在调用 layoutIfNeeded 时,view 必须要被 setNeedsLayout 后,才会理解执行 layoutSubviews。

一个视图缺少高宽约束,在设置完了约束后执行layoutIfNeeded,然后设置宽高,这种情况在低配机器上可能会出现崩问题。原因在于layoutIfNeeded需要有标记才会立刻调用layoutSubview得到宽高,不然是不会马上调用的。页面第一次显示是会自动标记上需要刷新这个标记的,所以第一次看显示都是看不出问题的,但页面再次调用layoutIfNeeded时是不会立刻执行layoutSubview的(但之前加上setNeedsLayout就会立刻执行),这时改变的宽高值会在上文生命周期中提到的Auto Layout Cycle中的Engine里的Deferred Layout Pass里执行layoutSubview,手动设置的layoutIfNeeded也会执行一遍layoutSubview,但是这个如果发生在Deferred Layout Pass之后就会出现崩的问题,因为当视图设置为setTranslatesAutoresizingMaskIntoConstraints:NO时会严格按照约束->Engine->显示这种流程,如在Deferred Layout Pass之前设置好是没有问题的,之后强制执行LayoutSubview会产生一个权重和先前一样的约束在类似动画block里更新布局让Engine执行导致Ambiguous Layouts这种权重相同冲突崩溃的情况发生。

把约束用约束中两个view的共同父视图或者两视图中层次高视图的- addConstraint:(NSLayoutConstraint *)constraint方法将约束添加进去。

约束关系
  • Auto Layout提供三种约束关系:>=、=、<=,分别对应NSLayoutRelationLessThanOrEqual、NSLayoutRelationEqual、NSLayoutRelationGreaterThanOrEqual。即线性方程不一定是等式,也可以是不等式。
// View的高度<=100
NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:greenView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationLessThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100];

RemoveFromSuperView

将多个有相互约束关系视图removeFromSuperView后更新布局在低配机器上出现崩的问题。这个原因主要是根据不含视图项的约束不合法这个原则来的,同时会抛出野指针的错误。在内存吃紧机器上,当应用占内存较多系统会抓住任何可以释放heap区内存的机会视图被移除后会立刻被清空,这时约束如果还没有被释就满足不含视图项的约束会崩的情况了。

remove 之前最好能够 clearConstraint。

先举个简单的例子并排两个view添加约束

约束优先级
  • 无论是我们创建的约束,还是系统创建的约束(IntrinsicContentSize相关的约束由系创建,下文会涉及),都必须指定一个约束优先级UILayoutPriority。默认创建出来的约束优先级为UILayoutPriorityRequired(1000),称为必需约束;其他优先级小于1000的约束称为可选约束。Auto Layout Engine进行约束解析时,尝试着按优先级从高到低满足约束集合中的每一个约束,如果无法满足某个可选约束,则跳过;当优先级不同的两个约束描述的是同一个布局关系,Auto Layout会跳过优先级较低的约束。

Apple官方文档表示:可选约束因无法满足被跳过时,仍旧可能影响布局。当约束冲突时,Auto Layout会选择相对接近的解,选择打破某些约束。在这个选择过程中,被跳过可选约束同样能影响选择最终结果。

我的一些疑问

第一步:updating constraints,被称为测量阶段,其从下向上(from subview to super view),为下一步layout准备信息。

始终不明白为什么要从下往上更新约束,我的理解是,先是父视图确定自己位置,子视图才确认自己视图。当然这个是 layout 的过程。

我当前的理解是这样,子视图的约束先更新,再逐步向上触发父视图更新约束,猜测的原因是子视图的约束可能会导致父视图约束更改。

[NSLayoutConstraint constraintWithVisualFormat:@“[view1]-[view2]" options:0 metrics:nil views:viewsDictionary;
约束的安装与移除
  • 1.使用NSLayoutConstraint创建一个约束对象,必须把约束添加到对应的位置,Apple规定约束必须添加到该约束相关的视图所在的视图树的第一个公共祖先(第一个公共superView),以下通过几个图示说明
    • 约束表示视图本身尺寸(width/height),则直接添加到该视图本身;约束表示两个视图的布局关系,则添加多着两个视图所在的视图树的第一个公共祖先;
    • 约束移除,使用removeConstraint/removeConstraints移除约束;当一个视图调用removeFromSuperView,与该视图相关的全部约束都会自动移除。
// 约束安装示例
UIView *view1 = [[UIView alloc] init];
[self.view addSubview:view1];    
view1.translatesAutoresizingMaskIntoConstraints = NO;

NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100];
NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100];

[self.view addConstraints:@[left, top]];

新葡京8455 3

图片出处 : WWDC2012 - Introduction to Auto Layout for iOS and OS X

新葡京8455 4

图片出处 : WWDC2012 - Introduction to Auto Layout for iOS and OS X

新葡京8455 5

图片出处 : WWDC2012 - Introduction to Auto Layout for iOS and OS X

  • 2.使用Interface Builder方式进行布局,在xib、Storyboard中拖线实现布局的,约束会自动添加到对应的位置,无需考虑约束安装问题。

  • 3.iOS8 ,Auto Layout推出新的接口。NSLayoutConstraint多了一个active属性,用于激活、失效一个约束。不需要再考虑约束安装位置。原本用于添加、移除约束的接口addConstraint/addConstraints、removeConstraint/removeConstraints,接口文档表示在后续的版本升级将会过期,建议避免使用,转而使用NSLayoutConstraint的active/activateConstraints、deactivateConstraints。

    笔者看了一下Masonry源码,作者在安装约束的install方法中做了版本适配,判断NSLayoutConstraint是否响应active方法。如果响应,则直接使用active方法安装、移除约束;否则使用传统方式,获取约束等式中两个view第一个公共祖先,找到约束应该添加的位置。

- (void)install {

    if (self.hasBeenInstalled) {
        return;
    }

    // 1.响应active方法,直接激活

    if ([self supportsActiveProperty] && self.layoutConstraint) {
        self.layoutConstraint.active = YES;
        [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
        return;
    }

    // 2.否则,使用传统方式,把约束添加到对应位置

    // 利用各个元素,构造一个完整的约束对象 MASLayoutConstraint
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

    // 兼容省略写法,例如 make.left.equalTo(@10),firstViewAttribute不是尺寸,却没有secondViewAttribute,则默认secondViewAttribute的secondLayoutItem是父view、secondLayoutAttribute与firstLayoutAttribute一致
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        secondLayoutItem = self.firstViewAttribute.view.superview;
        secondLayoutAttribute = firstLayoutAttribute;
    }

    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];

    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;

    // 获取约束安装位置installedView :firstViewAttribute.view 和secondViewAttribute.view的最近公共祖先
    if (self.secondViewAttribute.view) {
        MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
        NSAssert(closestCommonSuperview,
                 @"couldn't find a common superview for %@ and %@",
                 self.firstViewAttribute.view, self.secondViewAttribute.view);
        self.installedView = closestCommonSuperview;
    } else if (self.firstViewAttribute.isSizeAttribute) {
        self.installedView = self.firstViewAttribute.view;
    } else {
        self.installedView = self.firstViewAttribute.view.superview;
    }

    //根据标识位,执行约束更新、安装;把安装完毕的约束存储到view的mas_installedConstraints集合中
    MASLayoutConstraint *existingConstraint = nil;
    if (self.updateExisting) {
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
    }

    if (existingConstraint)      // 更新操作:更新常量即可
    {
        // just update the constant
        existingConstraint.constant = layoutConstraint.constant;
        self.layoutConstraint = existingConstraint;
    }
    else // 安装操作
    {
        [self.installedView addConstraint:layoutConstraint];
        self.layoutConstraint = layoutConstraint;
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
}

参考引用

  • 新葡京8455,https://objccn.io/issue-3-5/

viewDictionary可以通过NSDictionaryOfVariableBindings方法得到

三、Auto Layout多种使用方式

UIView *view1 = [[UIView alloc] init];UIView *view2 = [[UIView alloc] init];viewsDictionary = NSDictionaryOfVariableBindings(view1,view2);

1.NSLayoutConstraint

options

可以给这个位掩码传入NSLayoutFormatAlignAllTop使它们顶部对齐,这个值的默认值是NSLayoutFormatDirectionLeadingToTrailing从左到右。可以使用NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom 表示两个视图的顶部和底部约束相同。

NSLayoutConstraint对象创建约束
  • 原生的NSLayoutConstraint进行布局,使用NSLayoutConstraint提供的约束对象创建接口,传入对应的参数即可,一个约束对象对应一个布局关系。具体步骤如下
    • 设置View的translatesAutoresizingMaskIntoConstraints属性为NO
    • 根据约束方程式,创建约束对象;
    • 把约束添加到对应位置(iOS8 直接通过active激活约束);
  • NSLayoutConstraint接口设计,只有constant常量是readwrite,其他都是readonly属性,约束对象在创建的时候传入约束等式的各个参数,之后就只能修改约束常量(做动画时经常这么用)。

metrics

这个参数作用是替换VFL语句中对应的值

CGRect viewFrame = CGRectMake(50, 50, 100, 100);NSDictionary *views = NSDictionaryOfVariableBindings(view1, view2);NSDictionary *metrics = @{@"left": @(CGRectGetMinX(viewFrame)), @"top": @(CGRectGetMinY(viewFrame)), @"width": @(CGRectGetWidth(viewFrame)), @"height": @(CGRectGetHeight(viewFrame))};[view1 addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];

使用NSDictionaryOfVariableBindings快速创建

NSNumber *left = @50;NSNumber *top = @50;NSNumber *width = @100;NSNumber *height = @100;NSDictionary *views = NSDictionaryOfVariableBindings(view1, view2);NSDictionary *metrics = NSDictionaryOfVariableBindings(left, top, width, height);[view1 addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];
NSLayoutConstraint优缺点讨论
  • 使用该方式进行布局,最明显的特点是代码量冗余,欠优雅。
  • 约束更新、删除时,执行起来也不方便,需要实现用指针记录约束对象、或者通过匹配找到对应的约束。

VFL几个基本例子

  • [view1]-10-[view2] 表示view1宽50,view2宽100,间隔10
  • [view1(>=50@750)] 表示view1宽度大于50,约束条件优先级为750(优先级越大优先执行该约束,最大1000)
  • V:[view1][view2] 表示按照竖直排,上面是view1下面是一个和它一样大的view2
  • H:|-[view1]-[view2]-[view3]-| 表示按照水平排列,|表示父视图,各个视图之间按照默认宽度来排列

无论使用哪种方法创建约束都是NSLayoutConstraint类的成员,每个约束都会在一个Objective-C对象中存储y = mx b规则,然后通过Auto Layout引擎来表达该规则,VFL也不例外。VFL由一个描述布局的文字字符串组成,文本会指出间隔,不等量和优先级。官方对其的介绍:Visual Format Language

  • 标准间隔:[button]-[textField]
  • 宽约束:[button]
  • 与父视图的关系:|-50-[purpleBox]-50-|
  • 垂直布局:V:[topField]-10-[bottomField]
  • Flush Views:[maroonView][buleView]
  • 权重:[button]
  • 等宽:[button(==button2)]
  • Multiple Predicates:[flexibleButton(>=70,<=100)]
示例代码

新葡京8455 6

Snip20170917_25.png

UIView *grayView = [[UIView alloc] init];
grayView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:grayView];    
grayView.translatesAutoresizingMaskIntoConstraints = NO;

NSLayoutConstraint *left = [NSLayoutConstraint constraintWithItem:grayView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:50];
NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:grayView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:100];
NSLayoutConstraint *width = [NSLayoutConstraint constraintWithItem:grayView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100];
NSLayoutConstraint *height = [NSLayoutConstraint constraintWithItem:grayView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:100];

[self.view addConstraints:@[left, top]];
[grayView addConstraints:@[width, height]];

//  iOS8 
//  left.active = YES;
//  top.active = YES;
//  width.active = YES;
//  height.active = YES;

注意事项

创建这种字符串时需要注意一下几点:

  • H:和V:每次都使用一个。
  • 视图变量名出现在方括号中,例如[view]。
  • 字符串中顺序是按照从顶到底,从左到右
  • 视图间隔以数字常量出现,例如-10-。
  • |表示父视图
  • 注意禁用Autoresizing Masks。对于每个需要使用Auto Layout的视图需要调用setTranslatesAutoresizingMaskIntoConstraints:NO
  • VFL语句里不能包含空格和>,<这样的约束
  • 布局原理是由外向里布局,最先屏幕尺寸,再一层一层往里决定各个元素大小。
  • 删除视图时直接使用removeConstraint和removeConstraints时需要注意这样删除是没法删除视图不支持的约束导致view中还包含着那个约束(使用第三方库时需要特别注意下)。解决这个的办法就是添加约束时用一个局部变量保存下,删除时进行比较删掉和先前那个,还有个办法就是设置标记,constraint.identifier = @“What you want to call”。

表达布局约束的规则可以使用一些简单的数学术语,如下表

类型 描述
属性 视图位置 NSLayoutAttributeLeft, NSLayoutAttributeRight, NSLayoutAttributeTop, NSLayoutAttributeBottom
属性 视图前面后面 NSLayoutAttributeLeading, NSLayoutAttributeTrailing
属性 视图的宽度和高度 NSLayoutAttributeWidth, NSLayoutAttributeHeight
属性 视图中心 NSLayoutAttributeCenterX, NSLayoutAttributeCenterY
属性 视图的基线,在视图底部上方放置文字的地方 NSLayoutAttributeBaseline
属性 占位符,在与另一个约束的关系中没有用到某个属性时可以使用占位符 NSLayoutAttributeNotAnAttribute
关系 允许将属性通过等式和不等式相互关联 NSLayoutRelationLessThanOrEqual, NSLayoutRelationEqual, NSLayoutRelationGreaterThanOrEqual
数学运算 每个约束的乘数和相加性常数 CGFloat值

约束引用两视图时,这两个视图需要属于同一个视图层次结构,对于引用两个视图的约束只有两个情况是允许的。第一种是一个视图是另一个视图的父视图,第二个情况是两个视图在一个窗口下有一个非nil的共同父视图。

哪个约束优先级高会先满足其约束,系统内置优先级枚举值UILayoutPriority

enum { UILayoutPriorityRequired = 1000, //默认的优先级,意味着默认约束一旦冲突就会crash UILayoutPriorityDefaultHigh = 750, UILayoutPriorityDefaultLow = 250, UILayoutPriorityFittingSizeLevel = 50,};typedef float UILayoutPriority;

具有instrinsic content size的控件,比如UILabel,UIButton,选择控件,进度条和分段等等,可以自己计算自己的大小,比如label设置text和font后大小是可以计算得到的。这时可以通过设置Hugging priority让这些控件不要大于某个设定的值,默认优先级为250。设置Content Compression Resistance就是让控件不要小于某个设定的值,默认优先级为750。加这些值可以当作是加了个额外的约束值来约束宽。

updateConstraints -> layoutSubViews -> drawRect

使用Auto Layout的view会在viewDidLayoutSubviews或-layoutSubview调用super转换成具有正确显示的frame值。

  • 改变frame.origin不会掉用layoutSubviews
  • 改变frame.size会使 superVIew的layoutSubviews调用
  • 改变bounds.origin和bounds.size都会调用superView和自己view的layoutSubviews方法

Auto Layout以下几种情况会出错

  • Unsatisfiable Layouts:约束冲突,同一时刻约束没法同时满足。系统发现时会先检测那些冲突的约束,然后会一直拆掉冲突的约束再检查布局直到找到合适的布局,最后日志会将冲突的约束和拆掉的约束打印在控制台上。
  • Ambiguous Layouts:约束有缺失,比如说位置或者大小没有全指定到。还有种情况就是两个冲突的约束的权重是一样的就会崩。
  • Logical Errors:布局中的逻辑错误。
  • 不含视图项的约束不合法,每个约束至少需要引用一个视图,不然会崩。在删除视图时一定要注意。
  • po [[UIWindow keyWindow] _autolayoutTrace]

参考官方文档:

  • 无共同父视图的视图之间相互添加约束会有问题。

  • 调用了setNeedsLayout后不能通过frame改变视图和控件

  • 为了让在设置了setTranslatesAutoresizingMaskIntoConstraints:NO视图里更改的frame立刻生效而执行了没有标记立刻刷新的layoutIfNeeded的方式是不可取的。

一个视图缺少高宽约束,在设置完了约束后执行layoutIfNeeded,然后设置宽高,这种情况在低配机器上可能会出现崩问题。原因在于layoutIfNeeded需要有标记才会立刻调用layoutSubview得到宽高,不然是不会马上调用的。页面第一次显示是会自动标记上需要刷新这个标记的,所以第一次看显示都是看不出问题的,但页面再次调用layoutIfNeeded时是不会立刻执行layoutSubview的(但之前加上setNeedsLayout就会立刻执行),这时改变的宽高值会在上文生命周期中提到的Auto Layout Cycle中的Engine里的Deferred Layout Pass里执行layoutSubview,手动设置的layoutIfNeeded也会执行一遍layoutSubview,但是这个如果发生在Deferred Layout Pass之后就会出现崩的问题,因为当视图设置为setTranslatesAutoresizingMaskIntoConstraints:NO时会严格按照约束->Engine->显示这种流程,如在Deferred Layout Pass之前设置好是没有问题的,之后强制执行LayoutSubview会产生一个权重和先前一样的约束在类似动画block里更新布局让Engine执行导致Ambiguous Layouts这种权重相同冲突崩溃的情况发生。

将多个有相互约束关系视图removeFromSuperView后更新布局在低配机器上出现崩的问题。这个原因主要是根据不含视图项的约束不合法这个原则来的,同时会抛出野指针的错误。在内存吃紧机器上,当应用占内存较多系统会抓住任何可以释放heap区内存的机会视图被移除后会立刻被清空,这时约束如果还没有被释就满足不含视图项的约束会崩的情况了。

Github地址:

Github地址:

可以参看我上篇文章《AutoLayout框架Masonry使用心得》:

完整记录可以到官方网站进行核对和查找:What’s New in iOS

苹果在这个版本引入Auto Layout,具备了所有核心功能。

  • NavigationBar,TabBar和ToolBar的translucent属性默认为YES,当前ViewController的高度是整个屏幕的高度,为了确保不被这些Bar覆盖可以在布局中使用topLayoutGuide和bottomLayoutGuide属性。
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-[view1]" options:0 metrics:nil views:view2];
  • Self Sizing Cells
  • UIViewController新增两个方法,用来处理UITraitEnvironment协议,UIKit里有UIScreen,UIViewController,UIView和UIPresentationController支持这个协议,当视图traitCollection改变时UIViewController时可以捕获到这个消息进行处理的。
- setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS; - (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS; 
  • Size Class的出现UIViewController提供了一组新协议来支持UIContentContainer
- systemLayoutFittingSizeDidChangeForChildContentContainer:container NS_AVAILABLE_IOS; - sizeForChildContentContainer:container withParentContainerSize:parentSize NS_AVAILABLE_IOS; - viewWillTransitionToSize:size withTransitionCoordinator:coordinator NS_AVAILABLE_IOS; - willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:coordinator NS_AVAILABLE_IOS;
  • UIView的Margin新增了3个API,NSLayoutMargins可以定义view之间的距离,这个只对Auto Layout有效,并且默认值为{8,8,8,8}。NSLayoutAttribute的枚举值也有相应的更新
//UIView的3个Margin相关API@property (nonatomic) UIEdgeInsets layoutMargins NS_AVAILABLE_IOS; @property (nonatomic) BOOL preservesSuperviewLayoutMargins NS_AVAILABLE_IOS; - layoutMarginsDidChange NS_AVAILABLE_IOS; //NSLayoutAttribute的枚举值更新NSLayoutAttributeLeftMargin NS_ENUM_AVAILABLE_IOS, NSLayoutAttributeRightMargin NS_ENUM_AVAILABLE_IOS, NSLayoutAttributeTopMargin NS_ENUM_AVAILABLE_IOS, NSLayoutAttributeBottomMargin NS_ENUM_AVAILABLE_IOS, NSLayoutAttributeLeadingMargin NS_ENUM_AVAILABLE_IOS, NSLayoutAttributeTrailingMargin NS_ENUM_AVAILABLE_IOS, NSLayoutAttributeCenterXWithinMargins NS_ENUM_AVAILABLE_IOS, NSLayoutAttributeCenterYWithinMargins NS_ENUM_AVAILABLE_IOS, 

2.VFL

UIStackView

苹果一直希望能够让更多的人来用Auto Layout,除了弄出一个VFL现在又弄出一个不需要约束的方法,使用Stack view使大家使用Auto Layout时不用触碰到约束,官方口号是“Start with Stack View, use constraints as needed”。 更多细节可以查看官方介绍:UIKit Framework Reference UIStackView Class Reference

Stack Views :

Stack View提供了更加简便的自动布局方法比如Alignment的Fill,Leading,Center,Trailing。Distribution的Fill,Fill Equally,Fill Proportionally,Equal Spacing。

如果希望在iOS9之前的系统也能够使用Stack view可以用sunnyxx的FDStackView

VFL创建约束
  • VFL,即Visual Format Language,可视化格式语言。这种布局方式,同样是使用NSLayoutConstraint类来创建约束。不同之处在于:上面演示的NSLayoutConstraint方式是基于线程方程式创建约束,一个约束对象表示一个约束关系。而VFL是使用字符串编码的方式创建约束,Auto Layout根据字符串创建对应的约束对象。VFL字符串中可以传入任意多个视图、可以表示任意多个布局关系,因此使用VFL创建的约束时一次创建一个约束集合,返回一个装着NSLayoutConstraint对象的数组。

NSLayoutAnchorAPI

新增这个API能够让约束的声明更加清晰,还能够通过静态类型检查确保约束的正常工作。具体可以查看官方文档

NSLayoutConstraint *constraint = [view1.leadingAnchor constraintEqualToAnchor:view2.topAnchor];
  • Auto Layout Guide
  • UIScrollView And Autolayout
  • IB中使用Auto Layout:Auto Layout Help
  • What’s New in iOS
  • WWDC 2012: Introduction to Auto Layout for iOS and OS X
  • WWDC 2012: Introducing Collection Views
  • WWDC 2012: Advanced Collection Views and Building Custom Layouts
  • WWDC 2012: Best Practices for Mastering Auto Layout
  • WWDC 2012: Auto Layout by Example
  • WWDC 2013: Interface Builder Core Concepts
  • WWDC 2013: Taking Control of Auto Layout in Xcode 5
  • WWDC 2015: Mysteries of Auto Layout, Part 1 内容包含了Auto Layout更高封装stack view的介绍
  • WWDC 2015: Mysteries of Auto Layout, Part 2 包含Auto Layout生命周期和调试Auto Layout的一些方法介绍
VFL优缺点讨论
  • VFL使用简短的字符串指定布局关系,对一个布局字符串中传入的视图个数、布局关系个数不做限制,约束代码简洁。
  • 某些约束关系无法使用VFL约束规则来描述,例如尺寸比例(A的宽度是B的宽度的2倍)。
  • 字符串编码的固有缺陷:Xcode无法在编译期间检查约束,只能在运行时生效,安全性低。Xcode中把字符串颜色设置为警告效果的红色,应该是表示这部分代码编译器无能为力,程序猿自求多福。
  • 企业开发一般不会使用VFL,此处仅做简单介绍,详见 Apple官方教程VFL。Auto Layout 约束冲突时Log信息一般是以VFL语言展示,最好要能读懂。
示例代码

新葡京8455 7

Snip20170917_19.png

UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
view1.backgroundColor = [UIColor redColor];
[self.view addSubview:view1];

UIView *view2 = [[UIView alloc] init];
view2.translatesAutoresizingMaskIntoConstraints = NO;
view2.backgroundColor = [UIColor blueColor];
[self.view addSubview:view2];

/*
     水平方向约束:
     view1.left = superView.left 50;
     view1.width = 100;
     view1.right 50 = view2.right;
     view2.right 50 = superVew.right;

     view1.top = view2.top;
     view1.bottom = view2.bottom;

     竖直方向
     view1.top = superView.top 100;
     view1.height = 100;
*/
NSArray<NSLayoutConstraint *> *horizontalConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view1(100)]-50-[view2]-50-|" options:(NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom) metrics:nil views:NSDictionaryOfVariableBindings(view1, view2)];
[self.view addConstraints:horizontalConstraint];

NSArray<NSLayoutConstraint *> *verticalConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view1(100)]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view1, view2)];
[self.view addConstraints:verticalConstraint];

3.Interface Builder

  • Apple建议开发中使用Interface Builder进行布局,使用该方式进行布局,Xcode自带约束冲突、约束歧义检查。这种布局方式开发速度相对较快,遇到比较复杂的布局也可以结合代码进行。笔者日常开发中是纯代码流,就不班门弄斧。详见 Apple官方教程Interface Builder

4.NSLayoutAnchor

NSLayoutAnchor创建约束
  • NSLayoutAnchor 布局锚点,提供一种比NSLayoutConstraint更方便、安全方案。NSLayoutAnchor作为UIView的属性,它与NSLayoutConstraint的布局属性的枚举类型NSLayoutAttribute一一对应,并细分为三个子类,分别是:
布局锚点类型 对应的子类 布局属性
X轴方向 NSLayoutXAxisAnchor leadingAnchor、trailingAnchor、leftAnchor、rightAnchor、centerXAnchor
Y轴方向 NSLayoutYAxisAnchor topAnchor、bottomAnchor、centerYAnchor、firstBaselineAnchor、lastBaselineAnchor
尺寸 NSLayoutDimension widthAnchor、heightAnchor
NSLayoutAnchor优缺点讨论
  • 通过把NSLayoutAnchor子类化,布局类、布局属性被细化,Xcode在编译期间进行布局检查,只有相同类型的布局属性才能互相约束,否则编译警告,优化了NSLayoutConstraint运行时才检查的缺陷,及早更正错误。注意leadingAnchor/trailingAnchor不可以与leftAnchor/rightAnchor匹配,NSLayoutAnchor没有进行区分,还是需要在runtime执行检查。
  • 创建约束接口更加安全,只有尺寸相关的布局锚点才需要设置multiplier参数(其他都是默认1.0),即NSLayoutDimension类型的约束创建接口才提供multiplier参数定制,避免错误使用。
  • 约束接口也更加简洁,一行代码即可搞定一个约束;无需考虑约束需要添加到哪里,直接使用active属性激活。布局效率已经接近Masonry。
  • NSLayoutAnchor不足之处是仅支持iOS9 ,不兼容更早的版本。NSLayoutAnchor布局接口相对简洁,苹果爸爸帮我们多做了一些脏活累活,Masonry以后应该会是一个更轻量级的库。
示例代码

新葡京8455 8

Snip20170920_68.png

    UIView *yellow = [[UIView alloc] init];
    yellow.translatesAutoresizingMaskIntoConstraints = NO;
    yellow.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:yellow];

    UIView *green = [[UIView alloc] init];
    green.translatesAutoresizingMaskIntoConstraints = NO;
    green.backgroundColor = [UIColor greenColor];
    [yellow addSubview:green];

    UIView *red = [[UIView alloc] init];
    red.translatesAutoresizingMaskIntoConstraints = NO;
    red.backgroundColor = [UIColor redColor];
    [yellow addSubview:red];

    CGFloat margin = 20;
    [yellow.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:margin].active = YES;
    [yellow.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-margin].active = YES;
    [yellow.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:100].active = YES;
    [yellow.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:-margin].active = YES;

    [green.leadingAnchor constraintEqualToAnchor:yellow.leadingAnchor constant:margin].active = YES;
    [green.trailingAnchor constraintEqualToAnchor:yellow.trailingAnchor constant:-margin].active = YES;
    [green.topAnchor constraintEqualToAnchor:yellow.topAnchor constant:margin].active = YES;
    [green.bottomAnchor constraintEqualToAnchor:red.topAnchor constant:-margin].active = YES;

    [red.leadingAnchor constraintEqualToAnchor:green.leadingAnchor].active = YES;
    [red.trailingAnchor constraintEqualToAnchor:green.trailingAnchor].active = YES;
    [red.bottomAnchor constraintEqualToAnchor:yellow.bottomAnchor constant:-margin].active = YES;
    [red.heightAnchor constraintEqualToAnchor:green.heightAnchor multiplier:2.0].active = YES;

5.Masonry

  • Masonry ,对应的Swift版本是SnapKit,企业开发中绝大多数都会使用该库作为布局方案。该库接口设计优雅,只需要少量代码即可实现布局、布局更新。Masonry提供的Demo中对该库使用提供了详细的代码示例,上手相对容易。
示例代码
// 实现NSLayoutAnchor相同的布局
    UIView *yellow = [[UIView alloc] init];
    yellow.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:yellow];

    UIView *green = [[UIView alloc] init];
    green.backgroundColor = [UIColor greenColor];
    [yellow addSubview:green];

    UIView *red = [[UIView alloc] init];
    red.backgroundColor = [UIColor redColor];
    [yellow addSubview:red];

    CGFloat margin = 20;

    [yellow mas_makeConstraints:^(MASConstraintMaker *make) {
        make.leading.equalTo(self.view).offset(margin);
        make.trailing.equalTo(self.view).offset(-margin);
        make.top.equalTo(self.view).offset(100);
        make.bottom.equalTo(self.view).offset(-margin);
    }];

    [green mas_makeConstraints:^(MASConstraintMaker *make) {
        make.leading.equalTo(yellow).offset(margin);
        make.trailing.equalTo(yellow).offset(-margin);
        make.top.equalTo(yellow).offset(margin);
        make.bottom.equalTo(red.mas_top).offset(-margin);
    }];

    [red mas_makeConstraints:^(MASConstraintMaker *make) {
        make.leading.equalTo(green);
        make.trailing.equalTo(green);
        make.bottom.equalTo(yellow).offset(-margin);
        make.height.equalTo(green).multipliedBy(2.0);
    }];

6.UIStackView

  • UIStackView是iOS9 推出的一个专门用于布局的UI控件,尤其擅长多个控件成行、成列布局。UIStackView提供了一种脱离约束的布局方案,约束不需要手动创建。实际上底层也是通过添加约束实现布局,Xcode调试工具 Debug View Hierarchy可以看到,添加到UIStackView中的控件都自动设置了约束。

新葡京8455 9

Snip20170919_49.png

UIStackView布局方案特点
  • UIStackView是iOS9推出的布局方案,理论上无法向下兼容iOS9以前的版本。百度团队维护的一个第三方库FDStackView,可以做到无缝向下兼容iOS9,因此项目即使支持iOS8也可以直接使用UIStackView,等到放弃iOS8了再移除该库即可,没有任何代码侵入性。

  • UIStackView不足之处是,复杂的布局方案UIStackView需要多层嵌套,还是需要结合手动设置约束的方式来实现复杂布局。UIStackView擅长多个控件成行、成列布局,Masonry也提供了专门用于布局一行、一列控件的接口。

UIStackView特殊性
  • UIStackView继承自UIView,当时该控件只用于布局,不参与图层树的渲染。设置UIStackView的UI样式不起作用,例如圆角、背景色……

  • 添加到UIStackView的控件,设置hidden状态为YES的View依旧存在arrangedSubviews中,但是不会展示出来,也不影响其他控件的布局;Auto Layout中,View设置hidden为YES后不会展示,但约束依旧存在,影响其他控件的布局。

  • UIView通过addArrangedSubview添加到arrangedSubviews中,也自动添加到视图树中;UIView通过removeArrangedSubview仅仅移出arrangedSubviews,不会从视图树中移除;UIView通过removeFromSuperview移出视图层级,则自动移出arrangedSubviews。

  • UIView的hidden属性本来是不可动画属性(not animatable),在UIStackView中只要UIView添加到arrangedSubviews,它的hidden就变成了可动画属性,动画效果即:View加入/移出StackView的动画。

  • 使用UIStackView布局,通常需要设置具有内在尺寸Intrinsic Content Size视图的内容吸附、压缩阻力优先级(下文会涉及)。

示例代码
  • 使用UIStackView布局,步骤如下
    • 最外层UIStackView与传统布局方案一样,手动设置约束;
    • 创建UIStackView,设置布局参数(布局方向、间距、对齐方式…);
    • 把需要布局的控件添加到UIStackView中,由arrangedSubviews管理;

新葡京8455 10

15057846355533.jpg

    UIStackView *stackView = [[UIStackView alloc] init];
    stackView.backgroundColor = [UIColor redColor];
    stackView.layer.cornerRadius = 50;
    stackView.layer.masksToBounds = YES;
    stackView.axis = UILayoutConstraintAxisVertical;
    stackView.distribution = UIStackViewDistributionFill;
    stackView.alignment = UIStackViewAlignmentFill;
    stackView.spacing = 8;

    self.stackView = stackView;
    [self.view addSubview:self.stackView];

    [stackView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(84, 10, 200, 10));
    }];

    UILabel *label = [[UILabel alloc] init];
    label.backgroundColor = [UIColor yellowColor];
    label.text = @"标题标题";
    label.textAlignment = NSTextAlignmentCenter;
    [self.stackView addArrangedSubview:label];

    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.image = [UIImage imageNamed:@"Snip20170906_172"];
    [imageView setContentHuggingPriority:249 forAxis:UILayoutConstraintAxisVertical];
    [imageView setContentCompressionResistancePriority:749 forAxis:UILayoutConstraintAxisVertical];

    [self.stackView addArrangedSubview:imageView];

    UIButton *btn = [[UIButton alloc] init];
    btn.backgroundColor = [UIColor yellowColor];
    [btn setTitle:@"按钮按钮" forState:UIControlStateNormal];
    [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [self.stackView addArrangedSubview:btn];
  • 关于UIStackView使用, 这篇博客 给出了详细的纯代码方式布局教程,苹果官方示例 也通俗易懂。

四、Auto Layout关键知识

1.Auto Layout布局原则

坚持一致的布局方式
  • 如上所示,Auto Layout布局方式有多种,不同开发者会采用不同的方式。纯代码布局一般会采用Masonry;也接触过不少同行采用Interface Builder方式。苹果的建议是选择一种合适的布局方式,并坚持一致性。
    • 倍数关系multipliers,优先考虑整数,再考虑小数;
    • 常数Constant,优先考虑正数,再考虑负数;
    • 布局顺序一般是从左到右、从上到下。符合阅读习惯,代码可读性更强。
创建充分的、可满足的约束
  • 约束集合必须是充分的、可满足的。约束不充分(欠约束)、不可满足(约束冲突)会造成视图错位、视图丢失等。要创建充分的、可满足的约束需要遵循一定的布局原则。
  • 每一个约束对象关联一至两个视图。当设置尺寸约束时,关联一个视图;当设置位置约束时,关联两个视图;
  • 多个约束之间逻辑上必须互不冲突。(eg:一个视图不能同时满足width =100、width=200两个约束)当约束冲突时,Auto Layout会根据自身算法,选择打破某些约束直到约束集合逻辑上互不冲突,这就存在不确定性。
  • 一个充分的布局在每个坐标轴上至少要有两个约束,即一个视图一共至少需要四个约束,否则视图的尺寸、位置确定。(eg:一个视图在X轴上要充分约束,可以是这几种情况其中之一:视图设置了left、right约束;视图设置了left、centerX约束;视图设置了left、width约束;视图设置了centerX、width约束……)。具有内在内容尺寸(IntrinsicContentSize)的视图表面上我们只需要设置两个约束,实际上它还是遵守了该原则,系统自动帮我们加另外两个约束(eg:UILabel只需设置位置约束,系统帮我们加上尺寸相关的两个约束)。
  • 当多个约束对象描述的是同一个几何关系,不会造成约束冲突。eg:当A.width=B.width,视图C的添加了两个宽度约束,C.width = A.width、C.width = B.width,两个约束产生的效果相同,即使都是最高优先级UILayoutPriorityRequired也不会有约束冲突。开发中避免这么使用,约束集合充分即可,过度了就存在约束冲突的隐患了

2.translatesAutoresizingMaskIntoConstraints

  • translatesAutoresizingMaskIntoConstraints接口文档介绍:决定是否把UIView的autoresizing mask转化为Auto Layout约束集合的Bool值。autoresizing mask是Auto Layout之前的一种布局方案,Apple为了向后兼容,允许前者转换成后者。translatesAutoresizingMaskIntoConstraints默认设为YES,即autoresizing mask将会转换auto layout对应的约束集合(对应的约束类是NSAutoresizingMaskLayoutConstraint),且这些约束已经完全确定了UIView的位置和尺寸,如果额外添加约束就会造成约束冲突。
  • 该属性默认为YES,使用Auto Layout布局时需要设置为NO。使用Masonry则不用手动设置,底层帮我们设置好了。
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {

    // 禁用Autoresizing
    self.translatesAutoresizingMaskIntoConstraints = NO;

    // 初始化constraintMaker,执行设置约束的block
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);

    // 安装约束
    return [constraintMaker install];
}

3.alignmentRect对齐矩形

alignmentRect简介
  • Apple接口文档对alignmentRect的解释大致如下,位于UIView.h中分类UIView (UIConstraintBasedLayoutLayering)。Auto Layout并不是根据frame来确定视图的大小、尺寸,而是根据对齐矩形alignmentRect。默认情况下alignmentRect与frame是一致的,除非子类重写了alignmentRectInsets方法。对齐矩形是视图的边界,它忽略视图的装饰元素,例如阴影、徽章……即视图布局时确定尺寸和位置的对齐矩形,默认是忽略阴影、徽章……

  • 示例:绿色UIView设置约束如下,最终视图的尺寸、位置是忽略阴影的,仅仅是根据视图alignmentRect

新葡京8455 11

Snip20171008_166.png

![Snip20171008_166]

UIView *greenView = [[UIView alloc] init];
greenView.backgroundColor = [UIColor greenColor];
[self.view addSubview:greenView];

[greenView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(self.view).offset(50);
    make.top.equalTo(self.view).offset(100);            
    make.width.height.equalTo(@100);
}];

// 添加阴影
CALayer *greenViewLayer = greenView.layer;
greenViewLayer.shadowColor = [UIColor lightGrayColor].CGColor;
greenViewLayer.shadowOpacity = 1.0;  // 此参数默认为0,即阴影不显示
greenViewLayer.shadowRadius = 5.0;
greenViewLayer.shadowOffset = CGSizeMake(20, 20);
alignmentRect应用场景
  • 当UI提供的图片、或者代码绘制的图片包含阴影、徽章等装饰元素时,一般情况下是要忽略装饰元素,根据核心区域做约束,此时需要调整alignmentRect来达到目的。

  • 如果视图是UIImageView,则可以通过UIImage的方法imageWithAlignmentRectInsets来调整对齐矩形,插入内边距;如果是UIView,则可以通过重写UIView的方法alignmentRectInsets,调整对齐矩形。

  • 示例:UI提供的图片包含阴影效果,上、左、右分别有12pt的阴影,alignmentRect要实现与系统默认做法一致(忽略阴影),需要调整alignmentRect内边距。

    • 此处要实现核心区域(不包括阴影)尺寸大小为60pt*120pt。图一不做任何处理,阴影效果作为alignmentRect的一部分参与布局,不满足需求;图二通过imageWithAlignmentRectInsets调整image,满足需求;图三通过重写alignmentRectInsets,调整alignmentRect,满足需求。代码详见DEMO
    • 当然了,也可以把视图宽度增大为60pt 12pt*2,高度增大为120pt 12pt,x、y减小12pt,布局时包含阴影,也实现同样的效果。

新葡京8455 12

UI切图上、左、右包含12pt阴影效果

新葡京8455 13

实现效果

alignmentRect可视化
  • Xcode8下(笔者在Xcode9下配置出现奔溃,原因未知),可以配置启动参数,开启对齐矩形可视化,具体配置过程如图,图中的黄色框框就是对齐矩形的区域,也就是布局最终产生的效果(位置、尺寸)。

新葡京8455 14

alignmentRect可视化 开启

新葡京8455 15

alignmentRect可视化 效果

4.内在内容大小IntrinsicContentSize

IntrinsicContentSize介绍
  • 一般情况下,视图要设置位置、尺寸约束才能正确布局,拥有内在内容大小的视图,只需要设置位置约束,不需要设置尺寸约束。Auto Layout会根据视图的自然尺寸,自动帮我们设置尺寸约束,这就是Intrinsic Content Size,它描述的是视图内容(文字、图片等)在不压缩不拉伸情况下展示出来的自然尺寸。

  • 只有部分视图具有IntrinsicContentSize,有IntrinsicContentSize的视图,一部分是只有width/height,一部分两者兼具,具体如图所示

    • UIView没有IntrinsicContentSize;
    • UISlider在iOS下只定义了width;
    • UILabel、UIButton、UISwitch、UITextField的IntrinsicContentSize同时存在width、height;
    • UITextView、UIImageView的IntrinsicContentSize是动态变化的;

新葡京8455 16

图片出处 :苹果官方文档 - Auto Layout Guide

  • IntrinsicContentSize是基于控件的当前内容的。

    • UILabel、UIButton的IntrinsicContentSize与视图文字数量、字体大小相关;没有设置内容之前,也有IntrinsicContentSize;
    • UIImageView是IntrinsicContentSize是动态变化的,当没有设置image没有IntrinsicContentSize((-1,-1)),当设置了image,则IntrinsicContentSize就是设置的image对应的Size;
    • UITextView的IntrinsicContentSize也是动态变化的,它相对复杂,与内容、是否可滚动、约束相关。
  • 没有IntrinsicContentSize的视图,例如UIView,默认IntrinsicContentSize是返回(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric),UIViewNoIntrinsicMetric是UIView中定义的一个常量,值为-1,表示没有内在内容大小。当然我们也可以自定义UIView,重写intrinsicContentSize方法,返回一个固定的CGSize,让自定义视图具备intrinsicContentSize。

IntrinsicContentSize原理分析
  • 视图的IntrinsicContentSize本质上还是通过约束来实现的,AutoLayout在每个坐标轴方向设置两个约束,分别是:contentHugging(内容吸附)、compressionResistance(压缩阻力),简称CHCR。图示显示的是X轴方向的IntrinsicContentSize约束,伪代码如下
    • 内容吸附向内挤压视图,使得视图匹配视图内容的自然大小,防止视图被拉伸、填充空白;
    • 压缩阻力向外拉伸视图,使得视图匹配视图内容的自然大小,防止视图被挤压、剪切;

新葡京8455 17

图片出处: 苹果官方文档 - Auto Layout Guide

// Compression Resistance
View.height >= 0.0 * NotAnAttribute   IntrinsicHeight
View.width >= 0.0 * NotAnAttribute   IntrinsicWidth

// Content Hugging
View.height <= 0.0 * NotAnAttribute   IntrinsicHeight
View.width <= 0.0 * NotAnAttribute   IntrinsicWidth
  • IntrinsicContentSize对应的约束对象是私有类NSContentSizeLayoutConstraint。通过Log打印,可以验证视图的IntrinsicContentSize对应的约束。
    • 设置了UIButton的位置约束,不设置尺寸约束,打印UIButton的约束集合,结果如下所示。
    • UIButtonX轴、Y轴方向分别有对应的NSContentSizeLayoutConstraint类型的约束对象
    • contentHugging(内容吸附)默认优先级为250、compressionResistance(压缩阻力)默认优先级为750。
UIButton *button = [[UIButton alloc] init];
[self.view addSubview:button];
[button setTitle:@"按钮按钮按钮" forState:UIControlStateNormal];

[button mas_makeConstraints:^(MASConstraintMaker *make) {
        make.bottom.equalTo(self.view).offset(-10);
        make.left.equalTo(self.view).offset(10);
}];

[self.view layoutIfNeeded];

for (id constrain in button.constraints) 
{
NSLog(@"%@;  %f  %f",constrain, (CGFloat)([[constrain valueForKey:@"compressionResistancePriority"] floatValue]), [[constrain valueForKey:@"huggingPriority"] floatValue]);
}

NSLog(@"UIButton %@", NSStringFromCGSize(button.intrinsicContentSize));

2017-09-25 09:33:00.640 AutoLayoutTheory[25066:6102718] <NSContentSizeLayoutConstraint:0x6000000b3ce0 UIButton:0x7ff52f113060.width == 111>;  750.000000  250.000000
2017-09-25 09:33:00.640 AutoLayoutTheory[25066:6102718] <NSContentSizeLayoutConstraint:0x6000000b3d40 UIButton:0x7ff52f113060.height == 34>;  750.000000  250.000000
2017-09-25 09:33:00.641 AutoLayoutTheory[25066:6102718] UIButton {111, 34}
IntrinsicContentSize应用
  • 具有IntrinsicContentSize的视图进行约束时只需设置位置 ,尺寸自动匹配内容,减少工作量。

  • 当存在多个具有IntrinsicContentSize视图,某些视图要保持原尺寸,只拉伸、压缩特定视图时,通过设置CHCR优先级来实现,它包括X、Y轴两个方向。

    • 当使用Interface Builder布局时,默认会把UIImageView 、UILabel的content-hugging优先级调整为251。Apple这么做原因应该是:大部分情况下,当与UITextField等共存,当需要拉伸时,一般是希望拉伸后者。
    • 通过代码设置CHCR优先级,可以把CHCR优先级调整为0~1000之间。不希望视图被拉伸则把content-hugging优先级调高;不希望视图被压缩则把compression-resistance优先级调高。Apple建议避免设置CHCR优先级为UILayoutPriorityRequired。大部分情况下,设置CHCR优先级目的是保护视图不被拉伸、压缩,只需要把CHCR优先级设置为解决UILayoutPriorityRequired(eg:999)即可实现。设置CHCR优先级为1000时,当存在约束冲突时,就无法通过拉伸、压缩具有IntrinsicContentSize的视图来避免。Apple设置content-hugging默认优先级为250,compression-resistance默认优先级为750的初衷,应该是:拉伸、压缩视图好过约束冲突造成的不确定性;拉伸视图好过压缩视图。建议项目中抽取自定义优先级常量。(当添加了视图尺寸约束,优先级设置为1000,并且把CHCR优先级调整为1000,最终结果不会产生约束冲突,而是显示视图尺寸约束对应的尺寸,笔者猜测是Apple底层做了处理,当约束优先级相同是,生效的时视图尺寸对应的约束,而不是IntrinsicContentSize对应的约束。)
    • 代码演示
    UILabel *label1 = [[UILabel alloc] init];
    label1.text = @"label1";
    label1.textColor = [UIColor blackColor];
    label1.backgroundColor = [UIColor greenColor];
    [self.view addSubview:label1];

    UITextField *textFeild = [[UITextField alloc] init];
    textFeild.placeholder = @"请输入文本";
    textFeild.textColor = [UIColor blackColor];
    textFeild.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:textFeild];

    [label1 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view).offset(10);
        make.bottom.equalTo(self.view).offset(-10);
    }];

    [textFeild mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(label1.mas_right).offset(10);
        make.right.equalTo(self.view).offset(-10);
        make.bottom.equalTo(label1);
    }];

    // 水平方向 label保持原尺寸,textFeild允许拉伸/压缩
    [label1 setContentCompressionResistancePriority:UILayoutPriorityRequired-1 forAxis:UILayoutConstraintAxisHorizontal];
    [label1 setContentHuggingPriority:UILayoutPriorityRequired-1 forAxis:UILayoutConstraintAxisHorizontal];
内在内容尺寸IntrinsicContentSize与自适应尺寸FittingSize
  • IntrinsicContentSize是Auto Layout的输入源。而FittingSize是Auto Layout的输出结果。上文提及:视图的IntrinsicContentSize本质上就是约束NSContentSizeLayoutConstraint,即IntrinsicContentSize是转化为约束集合的一部分,参与Auto Layout的布局计算。而一个视图的FittingSize是基于子视图的约束集合、内容,来计算出视图本身的尺寸,即FittingSize是Auto Layout的计算结果。自动计算高度的API - (CGSize)systemLayoutSizeFittingSize:(CGSize)targetSize NS_AVAILABLE_IOS(6_0);就是视图自上向下设置好子视图约束,视图即可计算出本身尺寸,实现自动撑开。
  • UIStackView看起来好像是具有IntrinsicContentSize,实际上并不是,他是基于FittingSize。当使用UIStackView进行布局时,通常需要频繁设置具有IntrinsicContentSize的子视图CHCR优先级来调整视图拉伸、压缩,但是调整UIStackView的CHCR优先级是无效的,因为它就不具有IntrinsicContentSize。UIStackView添加子视图、设置好布局参数之后,就能自动撑开。UIStackView是通过设置约束实现布局的,只是这部分约束是系统自动转化,无需手动设置,即UIStackView本质上就是根据子视图的约束集合、内容来计算出本身的尺寸。

5.Auto Layout与国际化

  • app国际化,即适配不同国家的语言,主要包括语种本地化、布局适配,此处主要讨论后者。
布局方向适配
  • 上文介绍NSLayoutAttribute布局属性提及:leading表示前边、trailing表示后边,在left-to-right language,leading=left、trailing=right。在right-to-left language,leading相当于right、trailing相当于left。苹果建议开发中避免使用left/right,而是使用leading/trailing,当设备语言切换到right-to-left languages时,所有视图都会自动实现镜面对称,从右到左布局。iOS9 可以通过UIView的属性semanticContentAttribute自定义是否需要实现镜面对称。苹果的官方文档、demo都是用leading/trailing。

新葡京8455 18

图片出处: 苹果官方文档 - Auto Layout Guide

  • 话说回来,Apple Developer提及的right-to-left language,也就Arabic、Hebrew两门语言。而支持Arabic(阿拉伯语)的Apple官网也是在2016年中才上线,App Store目前也还没支持right-to-left language,也就是说苹果爸爸家的APP都不支持。花大量的技术成本去适配,个人认为没有必要,毕竟也只有做梦能梦见阿拉伯人在使用自家的APP。当然了,如果是项目刚启动,可以规定使用leading/trailing,避免使用left/right。
布局测试
  • APP支持国际化需要注意:描述同一个内容,不同语种的字符长度不一致。对本地化文本进行测试时可以开启启动参数NSDoubleLocalizedStrings 设置为YES,配置字符串长度加倍,方便进行极端情况测试。具体配置如图

新葡京8455 19

15061736124730.jpg

  • 适配right-to-left language,测试界面做镜面对称的翻转时,需要在Setting中配置iPhone语言为对应语种。iOS10下配置路径是:设置-通用-语言与地区-iPhone语言-阿拉伯文……

五、Auto Layout布局周期

1.Auto Layout布局机制

新葡京8455 20

图片出处: WWDC2015 - Mysteries of Auto Layout, Part 2

  • 大多数人能够熟练使用Auto Layout进行布局,但不一定知晓它的布局过程。我们创建视图树、描述视图之间的约束、设置优先级、设置视图内容,Layout Engine计算出视图位置、尺寸,绘制出对应的图层。Auto Layout是个Black Box,当出现问题时,调试起来比较困难。
  • Auto Layout布局过程涉及延迟机制,并非一有约束更新就马上进行布局重绘,当有约束更改时,系统的默认做法是延迟更新,目的是实现批量更改约束、绘制视图,避免频繁遍历视图层级,优化性能。当更新约束太慢影响到后序代码逻辑,也可强制马上更新。

2.Auto Layout布局流程

新葡京8455 21

图片出处: WWDC2015 - Mysteries of Auto Layout, Part 2

  • 关于Auto Layout的布局流程,Apple给出图示如上:即Layout Cycle是一个在App运行循环RunLoop下循环执行的一个过程。
    • App启动后开启RunLoop,循环检测图层树中是否存在约束变化;
    • 当发生Constrints Change(直接or间接设置、更新、移除约束),RunLoop检测到约束变化;
    • RunLoop发现约束变化后,就会进入Deferred Layout阶段,视图的位置、尺寸值会在这个过程计算,设置到对应视图上,并绘制出来;
    • 执行完一轮布局,RunLoop会继续检查视图树的约束更新情况,当再次发现约束更新,则执行新一轮布局……
Constraints Change
  • Constraints Change过程包括两个步骤:约束更新;Layout Engine重新计算布局。

新葡京8455 22

图片出处: WWDC2015 - Mysteries of Auto Layout, Part 2

  • 1.约束更新

    • 约束作为输入,以线性方程集合的形式存放在Layout Engines中,当涉及到约束方程集合的更改,就属于Constraints Change。例如ctivate/deactivate约束对象(激活/失效,对应旧版本的安装/移除约束对象);修改约束常量constant;修改约束优先级priority(改变约束方程集合的排序关系);修改视图树的结构(eg:移除视图会自动移除与之相应的约束)
  • 2.Layout Engine重新计算布局

    • 约束表达式是由表示视图的位置、尺寸的变量构成,当约束更新后,Layout Engine重新计算布局,这些变量可能被更新。重新计算布局后,那些由于约束更新导致位置、尺寸发生改变的视图的父视图会被打上needing layout的标记,即调用视图的setNeedsLayout进行标记。此时视图新的frame已经存在Layout Engine中,但是视图还没有更新位置、尺寸。接下来Deferred Layout Pass将会被安排执行。
Deferred Layout Pass

新葡京8455 23

图片出处: WWDC2015 - Mysteries of Auto Layout, Part 2

  • Deferred Layout Pass过程包括两个步骤 Update Constraints ;Reassign View Frames。

新葡京8455 24

图片出处: WWDC2015 - Mysteries of Auto Layout, Part 2

  • Update Constraints约束更新
    • 这里是笔者学习过程中比较迷惑的地方,前面的Constraints Change过程已经更新约束,到了Deferred Layout Pass阶段,又需要更新约束。该步骤确保将要发生改变的视图能够在此时更新,在遍历视图树重新摆放视图之前及时更新。
    • 该步骤从下到上(即:从子视图到父视图,表面上子视图是添加到最顶部,是"上面",但是从数据结构角度是从视图树子点到父节点,因此是从下到上)遍历视图层级,调用UIView的updateConstraints(UIViewController对应updateViewConstraints,该方法的默认实现是调用[self.view updateConstraints]),更新视图。可以重写该方法监听这个过程,也可以调用setNeedsUpdateConstraints手动触发。自定义View重写updateConstraints,可以发现在调用之前,视图的needsUpdateConstraints属性为YES,调用完毕needsUpdateConstraints被标记为NO
    • 实际上这个阶段目的是让视图有机会在下一轮layout pass前及时更新约束。通常这个步骤并不是必须的。一般情况下我们在初始化时设置约束(eg:UIView的init方法、UIViewController的viewDidLoad方法……),在获取到数据、触发交互事件时更新约束,比较少在updateConstraints中更新约束。WWDC视频中Apple给了两个应用场景:当约束更新过慢时可以在updateConstraints方法中更新约束,得益于批处理,在此更新约束会比较快(实际上我们更常用layoutIfNeed强制立即布局);另一个场景是当约束需要更具多个参数进行配置,导致约束需要频繁更新,此时可以同一在updateConstraints方法中更新,避免多次冗余更新。

新葡京8455 25

图片出处: WWDC2015 - Mysteries of Auto Layout, Part 2

  • Reassign View Frames重新赋值视图frames,更新视图的位置、尺寸
    • 该步骤从上到下遍历视图层级,调用更新约束时被标记为needing layout的视图的layoutSubviews方法(UIViewController是对应viewWillLayoutSubviews),让方法调用者重新布局它的子视图(注意不是本身)。可以重写layoutSubviews进行监听。
    • 实际上这个阶段是从Layout Engine中把视图的位置、尺寸的值读取出来设置到对应的视图上(在Mac OS中是通过setFrame赋值,在iOS中是setBounds、setCenter)。重写layoutSubviews可以发现,视图本身frame在该方法调用前已经有值,调用后该值不变;子视图frame在该方法调用前是旧值,该方法调用完毕会赋上新值。

新葡京8455 26

图片出处: WWDC2012:Introduction to Auto Layout for iOS and OS X

3.Auto Layout布局流程总结

  • 学习完整个布局流程,笔者自我感觉没有理解透彻这个过程,只能算是了解基本流程、布局机制,欢迎大家发表见解。总而言是,Auto Layout布局是存在延迟的,视图的frame不会立即更新。

新葡京8455 27

图片出处: WWDC2015 - Mysteries of Auto Layout, Part 2

  • 对应重写layoutSubviews,Apple的建议是谨慎使用,除非使用Auto Layout无法搞定的布局需求,才考虑使用。这个一个微妙的时刻,有些视图layout完毕,有些即将layout。重写layoutSubviews需要记住几点
    • 必须调用super的实现 ([super layoutSubviews]);
    • 如果想要invalidate子视图的布局,需要在调用super的实现之前;
    • 不要在此调用setNeedsUpdateConstraints,因为update constraints pass已经过了,在此调用为时已晚;
    • 不要invalidate视图对应是子视图树以外的视图,该方法只应该对子视图树负责,操作子视图树以外的视图可能会造成循环布局;

References

  • 苹果官方文档:Auto Layout Guide
  • objc:Advanced Auto Layout Toolbox
  • WWDC2012:Introduction to Auto Layout for iOS and OS X
  • WWDC2015:Mysteries of Auto Layout, Part 2
  • Autolayout的第一次亲密接触
  • 书籍《iOS Auto Layout 开发秘籍(第2版)》

本文由新葡京8455发布,转载请注明来源

关键词: