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


Auto Layout Cookbook(自动布局使用手册)

Views with Intrinsic Content Size(利用固有尺寸布局)

本节通过实际例子演示如何利用固有尺寸消减约束,简化布局;通常需要配合外扩和内缩优先级同时使用。

源码详见Auto Layout Cookbook

Simple Label and Text Field(标签和文本框的简单组合)

本例布局一个标签和一个文本框组:前者宽度由文字决定;后者占据剩余空间。

图42

借助固有属性,只需五条约束,即可搞定布局。当然别忘了正确设置外扩和内缩优先级。

更多关于固有尺寸,外扩和内缩优先级的信息,详见章节Intrinsic Content Size(固有尺寸)

Views and Constraints(搭建布局)

拖拽控件到画布,填写标签内容和文本框占位文字,然后按照下图添加约束:

图43

  1. Name Label.Leading = Superview.LeadingMargin
  2. Name Text Field.Trailing = Superview.TrailingMargin
  3. Name Text Field.Leading = Name Label.Trailing + Standard
  4. Name Text Field.Top = Top Layout Guide.Bottom + 20.0
  5. Name label.Baseline = Name Text Field.Baseline
Attributes(设置属性)

文本框内缩优先级必须小于标签,才会被优先拉伸;IB会自动将前者设置为250,后者设置为251。可以在尺寸面板验证:

Name Horizontal hugging Vertical hugging Horizontal resistance Vertical resistance
Name Label 251 251 750 750
Name Text Field 250 250 750 750
Discussion(分析&讨论)

本布局垂直方向上只有两条约束(4和5),水平方向上只有三条约束(1,2和3)。回忆一下章节Creating Nonambiguous, Satisfiable Layouts(创建明确可满足的布局)提到的规则:对于一个视图来说,要创建明确可满足的布局,水平和垂直方向上各需要两条约束。这是因为固有尺寸定义了标签宽高,文本框高度,从而替代了缺少的三条约束。

为简单起见,我们假设文本框高度一定大于标签;并且,上方间距以文本框到控制器上方布局参照的距离为准;最后,由于二者都展示文字,所以通过基准线对齐。

外扩和内缩优先级会影响水平方向上哪个视图被优先拉伸。IB已经帮我们设置好了:标签内缩优先级为251;文本框为250。因此由后者填充剩余空间。

注意

如果空间太小,无法显示所有视图,还需要修改外扩优先级:其决定空间不足时,优先剪裁哪个视图。

修改本例的外扩优先级作为课后练习留给读者。如果标签文字过多,或字体过大,现有空间不足以完全显示时,布局会产生歧义。系统会随机打破一个约束,其中一个视图被裁剪。

理想情况下,布局不应超出当前空间——如果空间紧张,最好使用自适应布局。但如果需要支持多种语言,以及动态字体,那么视图尺寸就很难控制。修改外扩优先级是万全之策。

Dynamic Height Label and Text Field(高度动态变化的标签和文本框)

通过假设文本框高度总是大于标签,Simple Label and Text Field(标签和文本框的简单组合)的布局逻辑得以简化。然而这种假设并非永远正确:增大标签字号,其高度有可能大于文本框。

所以,最好以高度较大的视图的上方间距为准。使用常规字号时,效果与上例没有区别;然而,标签字号调大于等于36.0pt时,上方间距将基于标签计算。

图44

本例多少有些牵强:实际使用时,文本框字号往往会随着标签一起增大。然而,鉴于用户可以设置动态字体大小,那么,当动态字体控件和固定尺寸控件(如图片)一同展示时,本技巧就有了用武之地。

Views and Constraints(搭建布局)

视图结构同Simple Label and Text Field(标签和文本框的简单组合) 一致,但约束更复杂:

图45

  1. Name Label.Leading = Superview.LeadingMargin
  2. Name Text Field.Trailing = Superview.TrailingMargin
  3. Name Text Field.Leading = Name Label.Trailing + Standard
  4. Name Label.Top >= Top Layout Guide.Bottom + 20.0
  5. Name Label.Top = Top Layout Guide.Bottom + 20.0 (Priority 249)
  6. Name Text Field.Top >= Top Layout Guide.Bottom + 20.0
  7. Name Text Field.Top = Top Layout Guide.Bottom + 20.0 (Priority 249)
  8. Name label.Baseline = Name Text Field.Baseline
Attributes(设置属性)

文本框内缩优先级必须小于标签,才会被优先拉伸;IB会自动将前者设置为250,后者设置为251。可以在尺寸面板验证:

Name Horizontal hugging Vertical hugging Horizontal resistance Vertical resistance
Name Label 251 251 750 750
Name Text Field 250 250 750 750
Discussion(分析&讨论)

每个视图的上方间距各通过一对约束定义。必要约束规定到控制器上方布局参照的距离大于等于20.0pt;可选约束试图将这个距离固定在20.0pt。

对于较高的视图,两个约束都能满足,上方间距20pt。而对于较矮的视图,只满足必要约束,忽略可选约束。因此,视图高度变化,上方间距重新计算。

注意

可选约束优先级务必小于默认内缩优先级(250)。否则,内缩约束失效,视图拉伸,而非改变位置。

特别注意以基准线对齐的情况:基准线对齐仅在文本视图保持固有尺寸时有效,尺寸变化,约束失效(包括必要约束)。

Fixed Height Columns(定高组合)

接下来我们以Simple Label and Text Field(标签和文本框的简单组合)为基础,纵向排列一组标签和文本框。所有标签后边对齐;所有文本框前后对齐;水平位置以最长的标签为准。同Simple Label and Text Field(标签和文本框的简单组合)一样,我们假设文本框高度一定大于标签。

图46

Views and Constraints(搭建布局)

排布视图,随后按照下图添加约束:

图47

  1. First Name Label.Leading = Superview.LeadingMargin
  2. Middle Name Label.Leading = Superview.LeadingMargin
  3. Last Name Label.Leading = Superview.LeadingMargin
  4. First Name Text Field.Leading = First Name Label.Trailing + Standard
  5. Middle Name Text Field.Leading = Middle Name Label.Trailing + Standard
  6. Last Name Text Field.Leading = Last Name Label.Trailing + Standard
  7. First Name Text Field.Trailing = Superview.TrailingMargin
  8. Middle Name Text Field.Trailing = Superview.TrailingMargin
  9. Last Name Text Field.Trailing = Superview.TrailingMargin
  10. First Name Label.Baseline = First Name Text Field.Baseline
  11. Middle Name Label.Baseline = Middle Name Text Field.Baseline
  12. Last Name Label.Baseline = Last Name Text Field.Baseline
  13. First Name Text Field.Width = Middle Name Text Field.Width
  14. First Name Text Field.Width = Last Name Text Field.Width
  15. First Name Text Field.Top = Top Layout Guide.Bottom + 20.0
  16. Middle Name Text Field.Top = First Name Text Field.Bottom + Standard
  17. Last Name Text Field.Top = Middle Name Text Field.Bottom + Standard
Attributes(设置属性)

打开属性面板,设置如下。 注意,右对齐标签文本的好处是:无论标签长度如何变化,文本永远紧贴文本框显示。

View Attribute Value
First Name Label Text First Name
First Name Label Alignment Right
First Name Text Field Placeholder Enter first name
Middle Name Label Text Middle Name
Middle Name Label Alignment Right
Middle Name Text Placeholder Text Enter middle name
Last Name Label Text Last Name
Last Name Label Alignment Right
Last Name Text Field Placeholder Enter last name

每一组标签和文本框,前者内缩优先级应大于后者。IB已经自动设置,可以在尺寸面板验证:

Name Horizontal hugging Vertical hugging Horizontal resistance Vertical resistance
First Name Label 251 251 750 750
First Name Text Field 250 250 750 750
Middle Name Label 251 251 750 750
Middle Name Text Field 250 250 750 750
Last Name Label 251 251 750 750
Last Name Text Field 250 250 750 750
Discussion(分析&讨论)

简单来说,这个布局就是将Simple Label and Text Field复制三份,从上至下排列在一起。重点是如何对齐。

首先,右对齐标签文本在很大程度上简化了布局:接下来只需让齐标签后边对齐,使它们等宽,而无须顾及文本长度。因为外扩优先级大于内缩优先级,所以标签总是被拉伸;又因为所有标签等宽,所以最终宽度等由文本最长的标签固有尺寸决定。

标签前边已经相对于父视图留白约束,文本框后边也对应父视图留白。同时,行与行之间已经等宽,所以只需对齐标签后边或文本框前边即可。

做法有很多,这里我们选择让所有文本框等宽。

Dynamic Height Columns(动高组合)

本布局将Dynamic Height Label and Text Field(高度动态变化的标签和文本框)Fixed Height Columns(纵向排列一组标签和文本框)相结合,要点包括:

  • 标签后边对齐,长度以最长的标签为准;
  • 文本框等宽,前后对齐;
  • 文本框被拉伸,填充剩余空间,而非标签;
  • 行高以较高的视图为准;
  • 字体或文本改变,布局可以动态适应;

图48

Views and Constraints(搭建布局)

依照上例排布视图,随后按照下图添加约束:

图49

  1. First Name Label.Leading = Superview.LeadingMargin
  2. Middle Name Label.Leading = Superview.LeadingMargin
  3. Last Name Label.Leading = Superview.LeadingMargin
  4. First Name Text Field.Leading = First Name Label.Trailing + Standard
  5. Middle Name Text Field.Leading = Middle Name Label.Trailing + Standard
  6. Last Name Text Field.Leading = Last Name Label.Trailing + Standard
  7. First Name Text Field.Trailing = Superview.TrailingMargin
  8. Middle Name Text Field.Trailing = Superview.TrailingMargin
  9. Last Name Text Field.Trailing = Superview.TrailingMargin
  10. First Name Label.Baseline = First Name Text Field.Baseline
  11. Middle Name Label.Baseline = Middle Name Text Field.Baseline
  12. Last Name Label.Baseline = Last Name Text Field.Baseline
  13. First Name Text Field.Width = Middle Name Text Field.Width
  14. First Name Text Field.Width = Last Name Text Field.Width
  15. First Name Label.Top >= Top Layout Guide.Bottom + 20.0
  16. First Name Label.Top = Top Layout Guide.Bottom + 20.0 (Priority 249)
  17. First Name Text Field.Top >= Top Layout Guide.Bottom + 20.0
  18. First Name Text Field.Top = Top Layout Guide.Bottom + 20.0 (Priority 249)
  19. Middle Name Label.Top >= First Name Label.Bottom + Standard
  20. Middle Name Label.Top = First Name Label.Bottom + Standard (Priority 249)
  21. Middle Name Text Field.Top >= First Name Text Field.Bottom + Standard
  22. Middle Name Text Field.Top = First Name Text Field.Bottom + Standard (Priority 249)
  23. Last Name Label.Top >= Middle Name Label.Bottom + Standard
  24. Last Name Label.Top = Middle Name Label.Bottom + Standard (Priority 249)
  25. Last Name Text Field.Top >= Middle Name Text Field.Bottom + Standard
  26. Last Name Text Field.Top = Middle Name Text Field.Bottom + Standard (Priority 249)
Attributes(设置属性)

打开属性面板,设置如下。 注意,右对齐标签文本的好处是:无论标签长度如何变化,文本永远紧贴文本框显示。

View Attribute Value
First Name Label Text First Name
First Name Label Alignment Right
First Name Text Field Placeholder Enter first name
Middle Name Label Text Middle Name
Middle Name Label Alignment Right
Middle Name Text Placeholder Text Enter middle name
Last Name Label Text Last Name
Last Name Label Alignment Right
Last Name Text Field Placeholder Enter last name

每一组标签和文本框,前者内缩优先级应大于后者。IB已经自动设置,可以在尺寸面板验证:

Name Horizontal hugging Vertical hugging Horizontal resistance Vertical resistance
First Name Label 251 251 750 750
First Name Text Field 250 250 750 750
Middle Name Label 251 251 750 750
Middle Name Text Field 250 250 750 750
Last Name Label 251 251 750 750
Last Name Text Field 250 250 750 750
Discussion(分析&讨论)

我们将Dynamic Height Label and Text Field(高度动态变化的标签和文本框)Fixed Height Columns(纵向排列一组标签和文本框)中的技巧结合起来:正如前者一样,成对使用必要和可选约束,动态调整行间距;又像后者一样,右对齐标签文本,通过文本框等宽约束对齐各行。

注意

视图到控制器上方布局参照的距离为20.0pt,行间距为8.0pt。如果要求上方间距根据上方栏位存在与否动态调整——则必须添加额外约束。思路与Adaptive Single View(单个自适应视图)一致。具体实现作为课后作业留给读者。

不难看出,布局已经开始变得复杂。不过,有几种思路可以简化:首先,之前已经说过,尽量使用堆叠视图;此外,整合控件,分组布局。但不管哪种思路,本质都是分解复杂布局,降低管理难度。

Two Equal-Width Buttons(等宽按钮)

接下来我们布局两个等宽按钮:与屏幕底部对齐;平分横向空间。

图50

Views and Constraints(搭建布局)

对齐边缘时,以IB提示的蓝色虚线为准(这是系统给出的对齐建议)。 拖拽两个按钮到画布,以IB提示的蓝色虚线为准,沿着屏幕底部对齐它们。拉伸其中一个,填充横向空间,无需担心宽度不等,稍后更新frame即可。最后按照下图添加约束:

图51

  1. Short Button.Leading = Superview.LeadingMargin
  2. Long Button.Leading = Short Button.Trailing + Standard
  3. Long Button.Trailing = Superview.TrailingMargin
  4. Bottom Layout Guide.Top = Short Button.Bottom + 20.0
  5. Bottom Layout Guide.Top = Long Button.Botton + 20.0
  6. Short Button.Width = Long Button.Width
Attributes(设置属性)

为了观察frame变化,设置按钮背景颜色;为了证明文本不影响宽度,设置长短不一的按钮标题。打开属性面板,设置如下:

View Attribute Value
Short Button Background Light Gray Color
Short Button Title short
Long Button Background Light Gray Color
Long Button Title Much Longer Button Title
Discussion(分析&讨论)

我们只用了按钮固有高度,忽略其固有宽度。水平方向上,按钮等宽,填充横向空间;垂直方向上的思路和Two Equal-Width Views(两个等宽视图)一致:只有两条约束,而非四条。

长短不一的标题可以证明文本对布局的影响(这里没有任何影响)。

注意

本例中我们将按钮背景颜色设置为浅灰色,便于观察frame。默认,按钮和标签没有背景颜色(透明),所以很难察觉frame的变化。

Three Equal-Width Buttons(等宽按钮x3)

本例是Two Equal-Width Buttons(等宽按钮)的延伸,布局三个等宽按钮。

图52

Views and Constraints(搭建布局)

排布视图,随后按照下图添加约束:

图53

  1. Short Button.Leading = Superview.LeadingMargin
  2. Medium Button.Leading = Short Button.Trailing + Standard
  3. Long Button.Leading = Medium Button.Trailing + Standard
  4. Long Button.Trailing = Superview.TrailingMargin
  5. Bottom Layout Guide.Top = Short Button.Bottom + 20.0
  6. Bottom Layout Guide.Top = Medium Button.Bottom + 20.0
  7. Bottom Layout Guide.Top = Long Button.Bottom + 20.0
  8. Short Button.Width = Medium Button.Width
  9. Short Button.Width = Long Button.Width
Attributes(设置属性)

为了观察frame变化,设置按钮背景颜色;为了证明文本不影响宽度,设置长短不一的按钮标题。打开属性面板,设置如下:

View Attribute Value
Short Button Background Light Gray Color
Short Button Title short
Medium Button Background Light Gray Color
Medium Button Title Medium
Long Button Background Light Gray Color
Long Button Title Long Button Title
Discussion(分析&讨论)

多增加一个按钮,意味着增加三条约束(水平两条,垂直一条)。水平方向上的两条约束分别负责定义按钮水平位置和宽度,因为我们忽略其固有宽度。与此同时,固有高度负责定义按钮高度,所以仍需一条约束确定按钮垂直位置。

注意

通过IB能够快速创建等宽约束:同时选中三个按钮,打开固定工具(Pin Tool),选择Equal Widths,即可生成两条等宽约束。

Two Buttons with Equal Spacing(等间距按钮)

表面看来,本布局同Two Equal-Width Buttons(等宽按钮)相似。但是,这里的按钮宽度以较长的标题为准。如果空间足够,则两个按钮宽度都以标题较长的按钮的固有宽度为准;剩余空间分为三等份,填充间隙。

图54

iPhone上,竖屏时两种布局(等宽和等间距)看起来没区别;而横屏时(或屏幕更大的设备上,如iPad),二者区别就很明显了。

Views and Constraints(搭建布局)

拖拽两个按钮以及三个视图到画布上,穿插摆放。按照下图添加约束:

图55

  1. Leading Dummy View.Leading = Superview.LeadingMargin
  2. Short Button.Leading = Leading Dummy View.Trailing
  3. Center Dummy View.Leading = Short Button.Trailing
  4. Long Button.Leading = Center Dummy View.Trailing
  5. Trailing Dummy View.Leading = Long Button.Trailing
  6. Trailing Dummy View.Trailing = Superview.TrailingMargin
  7. Bottom Layout Guide.Top = Leading Dummy View.Bottom + 20.0
  8. Bottom Layout Guide.Top = Short Button.Bottom + 20.0
  9. Bottom Layout Guide.Top = Center Dummy View.Bottom + 20.0
  10. Bottom Layout Guide.Top = Long Button.Bottom + 20.0
  11. Bottom Layout Guide.Top = Trailing Dummy View.Bottom + 20.0
  12. Short Button.Leading >= Superview.LeadingMargin
  13. Long Button.Leading >= Short Button.Trailing + Standard
  14. Superview.TrailingMargin >= Long Button.Trailing
  15. Leading Dummy View.Width = Center Dummy View.Width
  16. Leading Dummy View.Width = Trailing Dummy View.Width
  17. Short Button.Width = Long Button.Width
  18. Leading Dummy View.Height = 0.0
  19. Center Dummy View.Height = 0.0
  20. Trailing Dummy View.Height = 0.0
Attributes(设置属性)

为了观察frame变化,设置按钮背景颜色;为了证明宽度以文本长度为准,设置长短不一的按钮标题。打开属性面板,设置如下:

View Attribute Value
Short Button Background Light Gray Color
Short Button Title short
Long Button Background Light Gray Color
Long Button Title Much Longer Button Title
Discussion(分析&讨论)

不难看出,布局已经开始变得复杂。必须说明,本例旨在展示特定技巧,实际开发中最好使用堆叠视图。

我们需要间距根据父视图尺寸变化,这意味着需要一组等宽约束控。但空间是没法约束的,所以只能使用视图占位。

我们利用三个视图代表三个间距,它们是空的UIView对象,高度为0pt,以减少对于视图结构的影响。

注意

请注意:占位视图相当消耗性能,大量使用,意味着需要大量内存去绘制它们,尽管没有任何内容。

此外,占位视图会参与视图结构响应链的组成。这意味着它们会响应消息:例如执行点击测试时(Hit Testing)。消息不慎被占位视图拦截,会造成许多难以发现的bug。

也可以使用UILayoutGuide表示间距。在自动布局中,这种轻量级的对象代表一个矩形区域。布局参照(Layout Guide)不具有任何显示属性,不参与视图结构,是封装控件和表示间距的理想选择。

不幸的是,布局参照无法通过IB添加;而混合布局(IB + 代码)又十分麻烦。就难易程度而言,占位视图要比布局参照简单(IB布局)。

本例中,大于等于约束限制按钮最小间距;相等约束保证按钮等宽,间距等宽。接下来的布局交由按钮的外扩和内缩优先级完成。若空间不足,则占位视图宽度为0pt,按钮均分空间(减去标准间距);空间增加,按钮宽度增加,直至达到较大按钮固有宽度,随后占位视图宽度开始增加,充剩余空间。

Two Buttons with Size Class-Based Layouts(根据尺寸类别布局)

接下来我们将根据尺寸类别创建两套布局:一套适用于Any-Any,按钮等宽,横向排列,同Two Equal-Width Buttons(两个等宽按钮)一致。

另一套适用于Compact-Regular,按钮纵向排列,如图所示:

图56

iPhone竖屏时按钮纵向排列;其他时候横向排列。

Constraints(搭建布局)

视图排布同“两个等宽按钮”。在Any-Any下,添加1到6号约束。

接着切换至Compact-Regular。

图57

卸载约束2和5,按照下图所示,添加约束7,8,9。

图58

  1. Short Button.Leading = Superview.LeadingMargin
  2. Long Button.Leading = Short Button.Trailing + Standard
  3. Long Button.Trailing = Superview.TrailingMargin
  4. Bottom Layout Guide.Top = Short Button.Bottom + 20.0
  5. Bottom Layout Guide.Top = Long Button.Botton + 20.0
  6. Short Button.Width = Long Button.Width
  7. Long Button.Leading = Superview.LeadingMargin
  8. Short Button.Trailing = Superview.TrailingMargin
  9. Long Button.Top = Short Button.Bottom + Standard
Attributes(设置属性)

为了观察frame变化,设置按钮背景颜色;为了证明文本不影响宽度,设置长短不一的按钮标题。打开属性面板,设置如下:

View Attribute Value
Short Button Background Light Gray Color
Short Button Title short
Long Button Background Light Gray Color
Long Button Title Much Longer Button Title
Discussion(分析&讨论)

借助IB,我们可以基于尺寸类别配置视图,视图属性及约束。水平和垂直方向上各有三种类型(Compact,Any或Regular)尺寸类别,共计九种:四种对应具体设备类型(Compact-Compact,Compact-Regular,Regulard-Compact以及Regular-Regular);其余为基础类型,对应两种或多种尺寸类别(Compact-Any,Regular-Any,Any-Compact,Any-Regular以及Any-Any)。

系统根据尺寸类别加载最合适的布局:Any-Any适用于所有尺寸;Compact-Any对应宽度紧凑,高度任意(的尺寸);Compact-Regular宽度紧凑,高度正常(的尺寸)。尺寸类别变化——如iPhone从竖屏到横屏——布局自动切换,伴有动画。

借助这一特性,我们可以为横竖屏iPhone创建不同布局;甚至区分iPhone和iPad布局。许多细节都可以针对尺寸类别区分配置,当然,区分越细,布局越复杂,设计和维护的难度越大。

务必谨记,每个可能出现的尺寸类别都必须对应一个合法布局。除了四个具体尺寸类别外,针对基础尺寸类别的布局可以应对两个或全部(四个)具体尺寸类别(译者:例如,任意设备竖屏时的尺寸类可能为Compact-Regular和Regular-Regular,那么只需针对Any-Regular制作一套布局即可)。总之,一个简单的原则:以Any-Any为基础创建默认布局;根据需要,切换至其他具体尺寸类别,分别调整。

如果布局复杂,动手之前可以使用尺寸类别九宫格进行分析:使用布局填充四角。从图中可以看出哪些约束在多个尺寸类别下都有效,进而找到尺寸类别和布局的最佳组合。(译者:Xcode8中貌似已经移除了这一特性,所以…。好吧, 其实我从来都没用过😂)

更多关于尺寸类别的信息,详见章节Debugging Auto Layout(Debug自动布局)