翻译@Auto Layout Guide(自动布局指南)


Getting Started(新手上路)

Anatomy of a Constraint(约束详解)

布局所需的约束通过关系等式表示。一个等式表示一条约束。我们的目标就是定义有且仅有唯一解的等式。

下图列举了一个等式:

图5

其所表示的约束规定红色视图的前边(leading)位于蓝色视图后边(trailing)后方8pt的位置。等式的组成部分如下:

  • Item 1(元素1)。等式的第一个元素——这里是红色视图。元素1不能为空。元素必须是视图(view)或布局参照(layout guide)。

    (译者:其实,还有一种对象可以参与约束的创建:UILayoutSupport。但苹果提供了两种创建约束的原生API,关系等式以及VFL,其只能参与后者。而且,VFL由于不易用,已经几乎被放弃。🤔)

  • Attribute 1(属性1):元素1被约束的属性——这里是红色视图的前边(leading)。

  • Relationship(关系):等式左边和右边的关系,有三种:等于(equal),大于等于(greater than or equal),小于等于(less than or equal)。这里是左边与右边相等。
  • Multiplier(系数):属性2的值会乘以这个浮点数——这里是1.0。
  • Item 2(元素2):等式的第二个元素——这里是蓝色视图。与元素1不同,可以为空。
  • Attribute 2(属性2):元素2被约束的属性——这里是蓝色视图的后边(trailing)。如果元素2为空,此处则为Not an Attribute
  • Constant(常量):浮点数偏移量——这里是8.0。其和属性2的值相加。

大多数约束定义界面上两个元素之间的关系。元素可以视图,也可以是布局参照(layout guide)。约束还可以表示同一个元素两个属性之间的关系。例如,设定视图宽高比。也可以为视图的宽高设置常量:此时,等式中第二个元素为空,第二个属性为Not An Attribute,系数为0.0。

Auto Layout Attributes(可约束的属性)

自动布局中,属性表示可约束的元素特征。总的来说,包括元素的四边(上下,前后),宽高,以及水平和垂直方向上的中点。文本元素还会有一个或多个基准线属性。

图6

查看所有属性,详见枚举NSLayoutAttribute

注意

虽然OS X和iOS都使用这个枚举,但它们对某些值的解释不同。所以,查看文档时,要注意区分平台。

Sample Equations(样例等式)

使用不同元素和属性创建等式,能够表示表示不同约束。例如两个视图的间距,相对大小,让视图一边对齐,甚至视图本身的宽高比。然而,并非所有的属性都可以配对使用。

属性分为两类:表示尺寸的(如宽高)和表示位置的(如前后,左右)。尺寸属性定义元素大小,与位置无关。位置属性定义元素的相对位置,与尺寸无关。

根据上述区别,有如下规则:

  • 不能根据位置属性约束尺寸属性。
  • 不能为位置属性设置常量。
  • 不能对位置属性使用”无意义”的系数(即除了1.0以外的值)。
  • 不能根据垂直位置属性约束水平位置属性,或反之。
  • 不能根据前后位置,约束左右位置,或反之。

如果没有参照,仅设置元素的上边为常量20.0pt毫无意义。因此约束位置时,参照是必须的。例如,元素的上边位于父视图上边下方20.0pt处。然而,元素的高度为20.0pt就没问题。更多信息,详见章节Interpreting Values(数值解释)

代码3-1例举了一组表示常见约束的等式。

注意

本章所有等式都使用伪代码表示。要查看具体代码,详见章节代码创建约束自动布局手册

代码3-1 表示常见约束的等式

  1. // 高度固定
  2. View.height = 0.0 * NotAnAttribute + 40.0
  3. // 两个按钮间距固定
  4. Button_2.leading = 1.0 * Button_1.trailing + 8.0
  5. // 对齐两个按钮的前边
  6. Button_1.leading = 1.0 * Button_2.leading + 0.0
  7. // 两个按钮等宽
  8. Button_1.width = 1.0 * Button_2.width + 0.0
  9. // 视图中心与父视图对齐
  10. View.centerX = 1.0 * Superview.centerX + 0.0
  11. View.centerY = 1.0 * Superview.centerY + 0.0
  12. // 视图宽高比固定
  13. View.height = 2.0 * View.width + 0.0

Equality, Not Assignment(相等,而非赋值)

注意,等式两端是相等关系,而非赋值。

解析时,系统不会将右边的值赋给左边。而是像解方程一样,计算等式成立时属性1和属性2的值。这意味着元素的顺序无关紧要。例如,代码3-2和代码3-1表示的约束其实一样。

代码3-2 颠倒等式中元素的顺序

  1. // 两个按钮间距固定
  2. Button_1.trailing = 1.0 * Button_2.leading - 8.0
  3. // 对齐两个按钮的前边
  4. Button_2.leading = 1.0 * Button_1.leading + 0.0
  5. // 两个按钮等宽
  6. Button_2.width = 1.0 * Button.width + 0.0
  7. // 视图中心与父视图对齐
  8. Superview.centerX = 1.0 * View.centerX + 0.0
  9. Superview.centerY = 1.0 * View.centerY + 0.0
  10. // 视图宽高比固定
  11. View.width = 0.5 * View.height + 0.0

注意

调整元素顺序的同时,也需要调整系数和常量的值。例如,常量从8.0变为-8.0;系数从2.0变为0.5。常量为0.0和系数为1.0时不需调整。

使用自动布局,一个问题可以有很多种解决方式。理想情况下,选择最能够体现我们意图的方式。然而,不同开发者对问题的理解不同,选择也会不同。不管怎样,选定一种,并坚持使用,能够避免很多问题。例如,本文遵循下述规则:

  1. 系数最好为整数,而非分数。
  2. 常量最好为正数,而非负数。
  3. 如果可能,(添加约束时)元素应该按照布局中的顺序出现:自上至下,自前至后。

Creating Nonambiguous, Satisfiable Layouts(创建明确,可满足的约束)

自动布局的关键在于表示约束的等式有且仅有唯一解。模糊的(non-ambiguous)约束有一个以上解。无法满足的(unsatisfiable)约束没有解。

总的来说,视图的尺寸和位置都必须得到约束。假设父视图的尺寸已经确定(例如,父视图是iOS中一个场景(scene)的根视图),那么要创建一个明确,可满足的布局,每个子视图的每个方向上各需要两条约束。不过,至于使用哪些约束,选择很多。例如,下面的三个布局都是明确,可满足的(只显示水平方向上的约束)。

图7

  • 第一个布局相对于父视图前边约束视图前边,视图固定宽度。据此,视图后边的位置得以确定。
  • 第二个布局相对于父视图前边约束视图前边,父视图后边约束视图后边。根据父视图的宽度,视图宽度得以确定。
  • 第三个布局相对于父视图前边约束视图前边,视图中心点与父视图中心点对齐。根据父视图的宽度,视图宽度及后边得以确定。

注意,每个布局中,子视图都有两条水平约束。完整定义了视图宽度和水平位置:即水平方向上的布局是明确,可满足的。然而,它们的效果并不相同。想象一下,如果父视图的尺寸改变,视图尺寸和位置会如何变化。

第一个布局,视图宽度不变。大多数时候,这并非期望的效果。实际上,一般不能将视图尺寸写死。自动布局是为创建动态布局而生的,如果写死,它就是去了意义。

也许很难一眼看出,但第二个和第三个布局的效果相同:不论父视图尺寸如何变化,视图与其的左右间距不变。然而,从使用上讲,这两个布局又不一样。第二个布局可能更好理解,但第三个布局可能更好用,特别需要中心对齐多个视图时。如前所述,我们要根据效果选择布局方式。

看一个更复杂的例子。假设iPhone上有两个视图要并排显示。它们同宽,且四周都留有空隙。即使设备旋转,布局不变。

下图显示设备在垂直和水平方向时的效果:

图8

至于如何添加约束,下图是一个直观的解决方案:

图9

约束如下:

  1. // 垂直约束
  2. Red.top = 1.0 * Superview.top + 20.0
  3. Superview.bottom = 1.0 * Red.bottom + 20.0
  4. Blue.top = 1.0 * Superview.top + 20.0
  5. Superview.bottom = 1.0 * Blue.bottom + 20.0
  6. // 水平约束
  7. Red.leading = 1.0 * Superview.leading + 20.0
  8. Blue.leading = 1.0 * Red.trailing + 8.0
  9. Superview.trailing = 1.0 * Blue.trailing + 20.0
  10. Red.width = 1.0 * Blue.width + 0.0

根据之前的规则,我们要布局两个视图,意味着4个水平约束,4个垂直约束。虽然(这种推测)并不绝对,但能够让我们迅速上手。更重要的是,只有这样两个视图的尺寸和位置同时得到定义,从而创建明确,可满足的布局。缺少任意一条约束,布局有歧义;添加额外约束,则可能产生冲突。

当然,答案不只有一个。下图中的也可以:

图10

没有参照父视图,而是根据红色视图约束蓝色视图的上下边。伪代码如下:

  1. // 垂直约束
  2. Red.top = 1.0 * Superview.top + 20.0
  3. Superview.bottom = 1.0 * Red.bottom + 20.0
  4. Red.top = 1.0 * Blue.top + 0.0
  5. Red.bottom = 1.0 * Blue.bottom + 0.0
  6. // 水平约束
  7. Red.leading = 1.0 * Superview.leading + 20.0
  8. Blue.leading = 1.0 * Red.trailing + 8.0
  9. Superview.trailing = 1.0 * Blue.trailing + 20.0
  10. Red.width = 1.0 * Blue.width + 0.0

仍然是两个视图,仍然是每个方向四条约束。布局仍然是明确,可满足的。

哪个更好?

两种布局方式都OK。但哪个更好呢?

不好意思,我们无法评判孰优孰劣。因为它们各有优缺点。

对于第一种方式,如果移除其中一个子视图,对布局影响较小。视图被移出视图结构时,与其相关的所有约束也会被移除。如果移除红色视图,蓝色视图就只剩三条约束,只需再添加一条即可。而第二种方式,红色视图的移除意味着蓝色视图只剩下一条约束。

另一方面,对于第一种方式,假设需要对齐所有子视图的上下边,必须保证所有约束的偏移量相同。修改一处,其他地方也要修改。

Constraint Inequalities(不等关系)

目前为止,我们所接触的约束都通过相等关系表示。然而,也可以使用不等关系:即关系的种类可以是相等(equal),大于等于(greater than or equal to),和小于等于(less than or equal to)。

举个例子,通过不等关系约束视图尺寸的最大值和最小值(代码 3-3)。

代码 3-3 视图尺寸的最大值和最小值

  1. // 设置最小宽度
  2. View.width >= 0.0 * NotAnAttribute + 40.0
  3. // 设置最大宽度
  4. View.width <= 0.0 * NotAnAttribute + 280.0

之前我们说过,视图每个方向上只需两条约束,但使用不等关系表示的约束不在此列。两个不等约束可以代替一个相等约束,见代码3-4。

代码 3-4 两个不等约束代替一个相等约束

  1. // 一个相等约束
  2. Blue.leading = 1.0 * Red.trailing + 8.0
  3. // 可以被两个不等约束替代
  4. Blue.leading >= 1.0 * Red.trailing + 8.0
  5. Blue.leading <= 1.0 * Red.trailing + 8.0

然而这种替代并不总是成立。例如,代码3-3中的不等约束只限制了视图宽度——并没有定义宽度的具体值。因此,视图宽度依然需要约束,但这个值必须在不等约束的范围内。

Constraint Priorities(约束优先级)

约束的默认优先级是必要(reqiured),即最高级。计算布局时,所有约束都必须被满足,否则就会出错。无法满足的约束,其信息会被输出至控制台;并被系统忽略,我们称之为”打破约束”。随后,布局重新计算。更多信息,详见章节Unsatisfiable Layouts(无法满足的约束)

优先级的范围从1到1000,小于1000是可选约束;1000代表必要约束。

计算布局时,系统会按照优先级从高到低满足约束:对于无法满足的可选约束,会跳过(译者:对于无法满足的必要约束,意味着冲突,则会打破)

没有被满足的可选约束仍然可以影响布局。如果布局因为跳过可选约束而产生歧义,那么系统会基于这个约束调整布局,选择最接近的值。因此,它更像是一种“趋势”。(译者:例如,有可选约束a == b,即使其无法被满足,系统也会尽量将abs(a - b)的值最小化。🤔)

可选约束经常和不等关系一起使用。例如,代码3-4中的不等约束可以有不同优先级。大于等于的约束优先级为必要(1000),小于等于的约束优先级较低(250)。这意味着它们的间距至少为8.0pt;如果存在其他高优先级约束,则它们的间距可以拉大,但在满足这些高优先级约束的前提下,系统又会尽量缩小二者的间距至8.0pt。

注意

优先级不一定必须是1000。事实上,系统预设了四档:低(250),中(500),高(750)以及必要(1000)。有时,为了防止优先级相同所产生的冲突,可以微调。但如果我们需要为每个约束的优先级设置具体值,那么布局方式很可能有问题。

更多关于iOS预设优先级的信息,详见枚举UILayoutPriority。至于OS X,详见布局优先级常量。

Intrinsic Content Size(固有尺寸)

目前为止,我们一直遵守“视图尺寸和位置都必须得到明确约束”的规则。然而,某些视图的尺寸可以通过其内容(Content)确定,称为固有尺寸(Intrinsic Content Size)。例如,按钮(UIbutton)的固有尺寸等于标题(titleLabel)尺寸加上四周间距。(译者:这里指的是按钮只有标题,没有图片的情况🤔)

并非所有视图都有固有尺寸。即便有,也可能只定义宽高中的一个。表3-1是一些例子:

表 3-1 常见视图的固有尺寸

视图 固有尺寸
UIView和NSView 没有
滑块控件 只限定宽度(iOS)
根据类型不同,分别限定宽,高或全部(OS X)
标签,按钮,开关和文本框 同时限定宽高
文本视图(text view)和图像视图(image view) 根据内容变化

固有尺寸取决于当前内容:对于标签(UILabel)和按钮(UIButton)来说,取决于文本和字体。其他视图的情况更为复杂:例如,空的图像视图(UIImageView)没有固有尺寸;添加图片后,等同于图片尺寸。

文本视图(UITextView)的固有尺寸受文本内容,能否滚动,及其他约束影响:可滚动时,没有固有尺寸;不可滚动时,等同于文本不折行时显示所需的空间(即假设不包含回车,那么所有文本都在一行显示,这时文本的尺寸就是视图的固有尺寸);如果宽度受到约束,那么其固有尺寸就是在这个宽度下完整显示文本所需的高度。

自动布局使用两条约束表示某个方向上的固有尺寸(宽或高):内缩(content hugging)和外扩(content compression resistance)。内缩即向内收缩的趋势,包裹内容;外扩即向外伸展的趋势,保证内容不受裁减。

图11

代码3-5使用不等式表示代表固有尺寸的四条约束。其中IntrinsicWidthIntrinsicHeight分别表示宽和高。

代表 3-5 代表固有尺寸的约束

  1. // 外扩
  2. View.height >= 0.0 * NotAnAttribute + IntrinsicHeight
  3. View.width >= 0.0 * NotAnAttribute + IntrinsicWidth
  4. // 内缩
  5. View.height <= 0.0 * NotAnAttribute + IntrinsicHeight
  6. View.width <= 0.0 * NotAnAttribute + IntrinsicWidth

这些约束有优先级之分。默认,内缩优先级为250,外扩优先级为750。因此,视图更倾向于外扩(即内缩约束被打破)。对于大多数视图来说,这是合理的。例如,拉伸按钮(UIButton)通常不会有问题,但缩小时,内容会被裁减。注意,界面编辑器(Interface Builder)有时会自动修改优先级,避免约束冲突。更多信息,详见Setting Content-Hugging and Compression-Resistance Priorities

固有尺寸能够使布局动态响应视图内容的变化,减少约束的数量。但要妥善处理内缩和外扩优先级。使用建议如下:

  • 同时拉伸多个视图,内缩优先级都一样,布局歧义。系统无法判断应该拉伸哪个视图。

    举例:标签(UILabel)遇到文本框(UITextField),通常拉伸后者,前者不变。因此,要降低文本框的水平内缩优先级(Horizontal Content Hugging Priority)。

    界面编辑器能够自动处理上述情况:标签的内缩优先级自动设置为251;代码布局时需要手动修改。

  • 无背景颜色的视图(例如按钮和标签)一旦尺寸失真,效果会很诡异。而且很难Debug,因为只能观察到文字位置不正确。增加内缩优先级,以防止这种情况发生。

  • 基准线约束仅在高度保持固有高度时有效。一旦针对高度进行拉伸或压缩,则无法对齐。(译者:我从未用过这个约束,所以这里的理解可能有误。🤔)
  • 诸如开关(UISwitch)一类的控件,最好始终使用固有尺寸。妥善设置优先级,防止变形。
  • 避免使用最高优先级(即”必要”)。尺寸错误总比约束冲突好。将优先级设置为999,在保证固有尺寸的同时,也给意外情况留下回旋余地。例如,实际空间与预期相差过大殊。

Intrinsic Content Size Versus Fitting Size(固有尺寸与契合尺寸)

(译者:契合寸可以通过UIView方法systemLayoutSizeFittingSize:获取🤔)

对于Auto Layout来说,固有尺寸是输入:其被转化为约束,描述视图尺寸。

契合尺寸是输出:根据约束计算尺寸。子视图通过约束作用于父视图,后者尺寸通过约束推算得出。

以堆叠视图(UIStackView)为例:没有约束时,子视图和属性决定其尺寸。乍一看,它似乎有固有尺寸:布局时只需定义位置,无需指定尺寸。可事实上,其尺寸由Auto Layout计算得出(输出),而非计算的依据(输入)。它的外扩和内缩优先级无效,因为不存在固有尺寸。

如果堆叠视图尺寸需要根据外部视图变化,那么要么在二者间添加约束,要么根据外部视图调整子视图的外扩和内缩优先级。

记号

Interpreting Values(数值解释)

自动布局中数值的单位是点(points)。然而,值的意义会根据不同属性和布局方向有不同的解释。

属性 数值含义 备注
高 / 宽(Height/Width) 视图尺寸 可以是常量,也可以相对于其他宽高定义。不能为负。
上边 / 下边 / 基准线(Top/Bottom/Baseline) 越靠近屏幕下方,值越大 相对于垂直中心,上边,下边和基准线定义。
前边 / 后边(Leading/Trailing) 越靠近后边,值越大。阅读方向自左至右时,越靠右值越大;相反,越靠左数值越大。 相对于前边,后边和水平中心定义。
左边 / 右边(Left/Right) 越靠右值越大。 相对于左边,右边和水平中心定义。
尽量使用前后,而非左右,以便布局根据阅读方向做出调整。
默认,阅读方向同设备当前语言一致。设置视图属性semanticContentAttribute,以规定视图内容是否需要根据阅读方向翻转。
水平 / 垂直中心(Center X/Center Y) 数值的含义取决所参照的属性 水平中心可以相对于水平中心,前后,左右定义。
垂直中心可以相对于垂直中心,上下,基准线定义。