1. 初识 Auto Layout

自动布局(Auto Layout )会根据对视图施加的约束(constraints),来动态计算视图层次结构中所有视图的大小和位置。使用这种基于约束的设计方法,你可以构建动态响应内部和外部变化的用户界面。

那什么是外部变化?什么又是内部变化呢?

当父视图(superview)的大小或形状发生更改时,将发生外部变化。每次更改后,你都必须更新视图层次结构的布局,以使可用空间得到最佳利用。常见外部变化如下:

  • 用户调整窗口大小(OS X)。
  • 用户进入或离开iPad(iOS)上的拆分视图。
  • 设备旋转(iOS)。
  • 活动的呼叫和录音栏的出现或消失(iOS)。
  • 需要支持不同的尺寸类型。
  • 需要支持不同的屏幕尺寸。

这些变化大多数发生在运行时(runtime),并且需要应用程序动态响应。支持不同屏幕尺寸等功能表示该应用程序对不同环境的适应性。创建自适应界面也可以使你的应用不同类型的设备上良好运行。另外,自动布局也是支持iPad上的 滑行视图(Slide Over)和 拆分视图(Split Views)的关键组件。

当用户界面中的视图或控件的大小更改时,就会发生内部变化。比如:

  • 应用程序显示的内容发生更改:新内容可能需要与旧内容不同的布局,比如需要显示文本或图像时。
  • 该应用程序支持国际化:国际化应用程序必须考虑不同语言、地区和文化的差异,使你的 UI 在支持的所有语言和区域中正确显示。具体而言有三个方面需要考虑。第一,当你将用户界面翻译成其他语言时,标签需要占用不同的空间。第二,用于表示日期和数字的格式也可能因地区而异。第三,更改语言会影响文本的大小和布局的组织。不同的语言使用不同的布局方向。英语从左到右,阿拉伯语和希伯来语从右到左。用户界面元素的顺序应与布局方向匹配。如果按钮在英语视图的右下角,则应该在阿拉伯语的左下角。
  • 该应用程序支持动态类型(iOS):比如用户在应用程序运行时更改字体大小,字体和布局都必须适应。

UI 布局主要有三种方法:纯编程,使用自动调整大小的蒙版(autoresizing masks)来自动化对外部更改的某些响应,或者使用AutoLayout(自动布局)。

纯编程定义视图的框架,能提供最大的灵活性和功能,可以按需进行任何更改。但这种灵活性的代价,则是设计、调试和维护所付出的大量精力

你可以通过纯编程为视图层次结构中的每个视图设置框架来布局用户界面。该框架定义了视图相对于父视图(superview)坐标系的原点、高度和宽度。要布置用户界面,必须计算视图层次结构中每个视图的大小和位置。发生更改时,你必须为所有受影响的视图重新计算框架。由于必须自己管理所有更改,所以你需要花费大量精力设计、调试和维护 UI。

当然,你可以使用自动调整大小的蒙版来减轻这种工作量。

自动调整大小的蒙版(autoresizing mask )定义了视图框架在其父视图(superview)框架更改时如何变化。这简化了适应外部变化布局的创建。缺点在于,自动调整大小的蒙版支持可能布局的相对较小的子集,即它无法提供纯编程方式的灵活性和功能。你需要通过使用自己的程序更改来增加自动调整大小掩码来构建复杂的 UI。此外,自动调整大小的蒙版仅适应外部更改。

而自动布局代表了一种全新的范例,你不需要考虑视图的框架,考虑它们之间的关系即可。

AutoLayout(自动布局)使用一系列约束(constraints)来定义用户界面,约束表示两个视图之间的关系。AutoLayout(自动布局)将根据这些约束计算每个视图的大小和位置,从而产生可动态响应内部和外部变化的布局。

虽然用于设计一组约束以创建特定行为的逻辑,与用于编写过程或面向对象的代码逻辑不同。但是掌握自动布局与掌握任何其他编程任务类似:首先了解基于约束的布局背后的逻辑,然后学习API。

自动布局
自动布局

2. 无约束自动布局

堆栈视图(Stack views) 提供了一种简单的方法实现自动布局,它没有引入复杂的约束。 一个堆栈视图就定义了 UI 元素的一行或一列,堆栈视图会根据元素的属性排列它们。

  • axis: (仅UIStackView) 定义堆栈视图的方向(垂直或水平)。
  • orientation: (仅NSStackView ) 定义堆栈视图的方向(垂直或水平)。
  • distribution: 定义沿轴的视图布局。
  • alignment: 定义垂直于堆栈视图轴的视图布局。
  • spacing: 定义相邻视图之间的空间。

要使用堆栈视图,请在 Interface Builder 中将垂直或水平堆栈视图拖到画布上。 然后将内容拖放到堆栈中。

如果对象(object)具有固有内容大小,则它会以其固有尺寸显示在堆栈中。 如果没有,Interface Builder 会提供默认大小。 你可以调整对象的大小,然后 Interface Builder 添加约束以保持其大小。

要进一步调整布局,您可以使用属性检查器(Attributes inspector)修改堆栈视图的属性。 如下示例使用了 8-point 间距和均等填充分布。

堆栈视图的布局也基于排列后的视图的内容环绕(content-hugging)和压缩阻力(compression resistance)优先级。 你可以使用尺寸检查器( Size inspector)修改它们。

你可以通过直接向布置的视图中添加约束来进一步修改布局。一般,如果视图的大小默认返回给定维度的固有内容大小,则可以安全地为该维度添加约束。

此外,你可以将堆栈视图嵌套在其他堆栈视图中以构建更复杂的布局。

通常,使用堆栈视图来管理尽可能多的布局。 仅在无法仅凭堆栈视图实现目标时才使用约束。尽管创造性地使用嵌套堆栈视图可以产生复杂的用户界面,但是你无法完全摆脱对约束的需求。 至少,你总是需要约束条件来定义最外层堆栈的位置或大小。

使用堆栈视图布局 UI 详见 UIStackView Class Referenceor NSStackView Class Reference.

3.约束剖析

3.1 视图层次结构的布局

视图层次结构的布局定义为一系列线性方程式

每个约束代表一个方程,你需要保证这些方程只有一个可能解。

在 UI 中,大多数约束定义了两个项目(view 或 Layout Guide)间的关系。 约束也可以定义单个项目的两个不同属性之间的关系,比如设置项目的长宽比。 你也可以为项目的高度或宽度指定一个常量值。 此时,Item2 保留为空白,Attribute 2 为“非属性”,Multifier 设置为0.0。

示例解释:RedView 的前边缘必须比 BlueView 的后边缘高8.0。

示例
示例

这个方程式包括:

  • Item1:方程式中的第一项,本例为RedView。该项必须是视图或布局指南(a view or a layout guide)。
  • Attribute 1:限制 在 Item1 上的属性,本例为 RedView 的前缘( leading edge)。
  • Relationship:左右两侧之间的关系,可以是等于、大于等于、小于等于。
  • Multiplier:乘数,本例为1.0。
  • Item 2:方程式中的第二项,本例为 BlueView,可以没有。
  • Attribute 2:限制在 Item2 上的属性,本例指 BlueView 的尾缘(trailing edge)。如果第二项留空,则为“非属性”。
  • Constant:常数,恒定的浮点偏移量,本例是8.0。

3.2 自动布局属性

在自动布局中,属性定义了可以约束的特征。 自动布局属性通常包括:四个边缘(Leading、Trailing、Top 和 Bottom),Height,Width以及Center X 和Center Y。 文本项(Text)还具有一个或多个 Baseline 属性。

Auto Layout Attributes
Auto Layout Attributes

3.3 示例方程

这些方程式的参数和属性很多,你可以用它们创建许多不同类型的约束。 比如,定义视图间的间距、对齐视图边缘、定义两个视图的相对大小、定义视图的宽高比等。 但是,并非所有属性都兼容。

属性分为尺寸属性(Size attributes)位置属性(location attributes)

尺寸属性用于指定项目的高度和宽度,位置属性用于指示项目相对于其他项目的位置(上、左、下、右)。

考虑到这些差异,以下规则是适用的:

  • 不能将尺寸属性限制为位置属性。
  • 不能将常量值分配给位置属性。
  • 不能将非同一性乘数(1.0以外的值)与位置属性一起使用。
  • 对于位置属性,不能将垂直属性限制为水平属性,不能将 leading 或 trailing 属性限制为 left 或 right 属性。

如果没有其他上下文,将项目的 Top 设置为恒定值20.0是没有意义的。 你必须始终定义与其他项目相关的项目的位置属性,比如在父视图的顶部下方20.0。 但是,将项目的高度设置为20.0是有效的。示例方程如下:

// 设置恒定的 高度(height)40.0
View.height = 0.0 * NotAnAttribute + 40.0
// 设置两个按钮之间的距离(固定值)8.0
Button_2.leading = 1.0 * Button_1.trailing + 8.0
// 左对齐两个 Button
Button_1.leading = 1.0 * Button_2.leading + 0.0
// 将两个按钮设置为等宽
Button_1.width = 1.0 * Button_2.width + 0.0
// 将某个视图置于其父视图的中央
View.centerX = 1.0 * Superview.centerX + 0.0
View.centerY = 1.0 * Superview.centerY + 0.0
// 设置恒定的宽高比(1:2)
View.height = 2.0 * View.width + 0.0

3.4 相等

注意,这里的“=”表示相等而非赋值。

当自动布局求解这些方程式时,它不仅将右侧的值分配给左侧。 它还会计算使关系成立的属性1和属性2的值。 这意味着,我们可以自由地对方程中的项目重新排序。这和数学上调整线性方程组中的方程一样,调换方程的位置、左右移项都不会影响方程组的解。

// 设置两个按钮之间的距离(固定值)8.0
Button_1.trailing = 1.0 * Button_2.leading - 8.0
// 左对齐两个 Button
Button_2.leading = 1.0 * Button_1.leading + 0.0
// 将两个按钮设置为等宽
Button_2.width = 1.0 * Button.width + 0.0
// 将某个视图置于其父视图的中央
Superview.centerX = 1.0 * View.centerX + 0.0
Superview.centerY = 1.0 * View.centerY + 0.0
// 设置恒定的宽高比(1:2)
View.width = 0.5 * View.height + 0.0

在重新排序项目时,请确保将乘数和常数取反。

例如,常数8.0变为-8.0。 2.0的乘数变为0.5。 常数0.0和乘数1.0保持不变。

在自动布局中,同一问题经常会有多种解决方法。 哪一种方案最能清楚地描述你的意图,你就选用哪一种。

下面是供参考的经验法则:

  • 整数乘数优于分数乘数。
  • 正常数优于负常数。
  • 视图应尽可能按布局顺序显示:从前到后,从上到下。

3.5 创建确定有效的布局

使用自动布局时,提供的一系列方程只有一个可能的解决方案,不应该多解(约束条件模棱两可)或无解(约束无效)。

通常,约束必须定义每个视图的尺寸和位置。 假设已经设置了父视图的大小(如iOS中的根视图),则一个确定、有效的布局每个维度的每个视图都需要两个约束(不包括父视图)。 但是,在具体选用约束时,你可以有多种选择。 例如,以下三个布局均产生确定、有效的布局(仅显示水平约束):

Creating Nonambiguous, Satisfiable Layouts
Creating Nonambiguous, Satisfiable Layouts
  • 布局 1(不推荐):将视图的 leading 相对于其父视图的 leading 进行约束。 它还为视图提供了固定的 width。 然后可以根据父视图的大小和其他约束条件来计算 trailing 的位置。
  • 布局 2:将视图的 leading 相对于其父视图的 leading 进行约束。 相对于父视图的 trailing,它也限制了视图的 trailing。 然后可以根据父视图的大小和其他约束条件来计算视图的 width。
  • 布局 3(推荐):将视图的 leading 相对于其父视图的 leading 进行约束。 它还使视图和父视图居中对齐。 然后,可以根据父视图的大小和其他约束条件来计算 width 和 trailing的位置。

请注意,每个布局都有一个视图和两个水平约束。在每种情况下,约束都完全定义了视图的宽度和水平位置。这意味着所有布局均沿水平轴生成确定、有效的布局。

但是,这些布局不是完全等同的,自适应能力也不一样。

让我们看看,当父视图的宽度改变时会发生什么。

布局 1:视图的 width 不变,应避免为视图指定固定的尺寸。自动布局旨在创建可动态适应其环境变化的布局。如果你给视图指定了固定大小,自动布局就没什么用了。

布局 2 和布局 3 效果相同:随着视图的宽度变化,视图与其父视图都保持固定的边距。它们不一定完全等效。

布局 2 易于理解,布局 3 可能更有用,尤其是在居中对齐多个项目时。

让我们继续深入分析。

假设你要在 iPhone 上同时显示两个视图。你需要确保,它们的所有边都具有合适边距,并且始终具有相同的宽度,设备旋转时也应正确调整大小。

纵向、横向显示:

portrait

landscape

那么这些约束应该是什么样的呢?

方案 1:此布局具有两个视图,四个水平约束和四个垂直约束。这些约束条件唯一地指定了两个视图的尺寸和位置,从而产生了一个确定、有效的布局。 约束刚刚好:删除所有这些约束,布局就会变得不确定;添加其他约束,就有引发冲突的风险。

a solution
a solution

上述解决方案使用了以下约束:

// 垂直约束
Red.top = 1.0 * Superview.top + 20.0
Superview.bottom = 1.0 * Red.bottom + 20.0
Blue.top = 1.0 * Superview.top + 20.0
Superview.bottom = 1.0 * Blue.bottom + 20.0
// 水平约束
Red.leading = 1.0 * Superview.leading + 20.0
Blue.leading = 1.0 * Red.trailing + 8.0
Superview.trailing = 1.0 * Blue.trailing + 20.0
Red.width = 1.0 * Blue.width + 0.0

方案 2:无需将蓝色框的顶部和底部固定到其父视图,而是将蓝色框的顶部与红色框的顶部对齐,将蓝色框的底部与红色框的底部对齐。该示例仍具有两个视图,四个水平约束和四个垂直约束。 它仍然可以产生确定、有效的布局。

another solution
another solution
// 垂直约束
Red.top = 1.0 * Superview.top + 20.0
Superview.bottom = 1.0 * Red.bottom + 20.0
Red.top = 1.0 * Blue.top + 0.0
Red.bottom = 1.0 * Blue.bottom + 0.0
//	水平约束
Red.leading = 1.0 * Superview.leading + 20.0
Blue.leading = 1.0 * Red.trailing + 8.0
Superview.trailing = 1.0 * Blue.trailing + 20.0
Red.width = 1.0 * Blue.width + 0.0

它们均能产生有效的布局, 那么哪个更好呢?我们比较一下方案 1 和方案 2。

删除视图时,方案 1 比方案 2 更加健壮。

从视图层次结构中删除视图也将删除所有引用该视图的约束。 因此,如果删除红色视图,则蓝色视图将保留三个约束,将其固定在适当的位置。 您只需要添加一个约束,就可以再次拥有有效的布局。 在方案 2 中,删除红色视图将使蓝色视图仅具有一个约束。

然而在方案 1 中,如果要使视图的顶部和底部对齐,则必须确保其顶部和底部约束使用相同的常量值。 如果更改一个常数,则必须记住也要更改另一个常数。

所以,很难说哪个方案是最好的,因为不同的业务场景和应用侧重的东西不同,你需要根据你的业务需求选择最合适的方案。

3.6 不等式约束

约束关系有三种:等于、小于等于、大于等于。

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

单个相等关系和一对不等式产生效果相同。

// 单个相等关系
Blue.leading = 1.0 * Red.trailing + 8.0
// 一对不等式
Blue.leading >= 1.0 * Red.trailing + 8.0
Blue.leading <= 1.0 * Red.trailing + 8.0

但逆不总成立,因为两个不等式并不总是等同于一个等式。 例如,不等式限制了视图宽度的可能值范围,但它们本身并没有定义宽度。 你仍然需要其他水平约束来定义该范围内视图的位置和尺寸。

3.7 约束优先级

默认情况下,所有约束都是必需的。自动布局必须计算出满足所有约束的解,否则会出错。Auto Layout (自动布局)将有关无法满足的约束的信息打印到控制台,并选择要打破的约束之一。然后,它会重新计算解决方案,而不会破坏约束。详见Unsatisfiable Layouts

你还可以创建可选约束。所有约束的优先级在1到1000之间。要求约束的优先级为1000。所有其他约束是可选的。

在计算解决方案时,自动布局会尝试按照从高到低的优先顺序满足所有约束。如果它不能满足可选约束,那么将跳过该约束,并继续到下一个约束。

即使不能满足可选约束,它仍然会影响布局。如果在跳过约束后布局中存在任何歧义,则系统将选择最接近约束的解决方案。

可选约束和不等式常常是分不开的。

比如你可以为两个不等式提供不同的优先级。可能需要大于等于关系(优先级为1000),小于等于关系具有较低的优先级(优先级250)。这意味着蓝色视图与红色视图的距离不能小于8.0。但是,其他限制可能会将其拉远。尽管如此,鉴于布局中的其他约束,可选约束将蓝色视图拉向红色视图,以确保其尽可能接近8.0间距。

不必强制使用所有1000个优先级值。实际上,优先级通常应围绕系统定义的低优先级(250),中优先级(500),高优先级(750)和所需优先级(1000)进行聚类。你可能需要设定比这些值高或低一两个点的约束。如果您要超出此范围,则可能需要重新检查布局的逻辑。

3.8 固有内容大小

到目前为止,所有示例都使用约束来定义视图的位置和大小。 但是,鉴于其当前内容,某些视图具有自然大小。 这称为其固有内容大小(Intrinsic content size)。 例如,按钮的固有内容大小是其标题的大小加上很小的空白。

并非所有视图都具有固有的内容大小。

对于包含视图的视图,固有内容大小可以定义视图的高度,宽度或两者。 下表列出了一些示例。

固有内容的大小基于视图的当前内容。标签或按钮的固有内容大小取决于所显示的文本量和所使用的字体。对于其他视图,固有内容的大小甚至更加复杂。例如,空图像视图没有固有的内容大小。不过,添加图片后,其固有内容大小将设置为图片的大小。

文本视图的固有内容大小取决于内容、是否启用滚动以及应用于视图的其他限制。例如,在启用滚动的情况下,视图没有固有的内容大小。在禁用滚动的情况下,默认情况下,视图的固有内容大小是根据文本大小计算的,没有任何换行符。例如,如果文本中没有返回值,它将计算将内容布置为单行文本所需的高度和宽度。如果添加约束以指定视图的宽度,则固有内容大小将定义显示给定宽度的文本所需的高度。

自动布局使用每个尺寸的一对约束来表示视图的固有内容大小。 内容环绕(content hugging)将视图向内拉,使其紧贴内容周围。 压缩阻力(compression resistance)将视图向外推,因此它不会剪切内容。

Text views
Text views

这些约束是使用如下所示的不等式定义的。IntrinsicHeight 和 IntrinsicWidth 常数表示视图的固有内容大小的高度和宽度值。

// 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

每一个约束中都有自己的优先级。默认情况下,视图使用250优先级表示 内容环绕(content hugging),并使用750优先级表示抗压缩性(compression resistance)。因此,拉伸视图要比缩小视图容易。对于大多数控件,这是我们想要的。例如,您可以安全地拉伸大于其固有内容大小的按钮;但是,如果缩小它,其内容可能会被剪切。请注意,Interface Builder有时会修改这些优先级以帮助防止联系(prevent ties)。

你应该尽可能在布局中使用视图的固有内容大小。它使布局可以随着视图内容的变化而动态地适应。它还减少了创建确定、无冲突的布局所需的约束数量,但你将需要管理视图的 Content-Hugging 和 Compression-Resistance (CHCR)优先级。以下是一些处理固有内容大小的准则:

  • 拉伸一系列视图以填充空间时,如果所有视图的内容环绕(content hugging)优先级都相同,则布局不确定。自动布局不知道应该拉伸哪个视图。比如标签和文本字段对。你希望文本字段拉伸以填充额外的空间,同时标签保持其固有内容大小。为确保这一点,请确保文本字段的水平内容环绕(content hugging)优先级低于标签的优先级。这种情况很普遍,Interface Builder 会自动处理,将所有标签的内容环绕(content hugging)优先级设置为251。如果你是以纯编程方式创建布局,则需要自己修改内容环绕(content hugging)优先级。
  • 当具有不可见背景的视图(如按钮或标签)被意外拉伸超出其固有内容大小时,通常会发生奇怪、意外的布局。实际的问题可能并不明显,因为文本只是出现在错误的位置。为防止不必要的拉伸,请增加内容环绕(content hugging)优先级。
  • Baseline 约束仅适用于处于其固有内容高度的视图。如果视图是垂直拉伸或压缩的,则基线约束将不再正确对齐。
  • 某些视图(如开关)应始终以其固有内容大小显示。根据需要增加其CHCR优先级,以防止拉伸或压缩。
  • 避免给视图提供CHCR优先级。通常,视图大小错误比不小心造成冲突更好。如果视图应始终是其固有内容大小,则可以考虑使用很高的优先级(999)。这种方法通常可以防止视图被拉伸或压缩,但仍可以提供紧急压力阀,以防万一您的视图显示在比预期更大或更小的环境中。

3.9 固定内容大小 Vs 恰当尺寸

固有内容大小用作 Auto Layout 的输入。当视图具有固有内容大小时,系统会生成表示该大小的约束,并使用约束来计算布局。

另一方面,恰当尺寸(fitting size)是 Auto Layout 引擎的输出。它是根据视图的约束为视图计算的大小。如果该视图使用 Auto Layout 布局其子视图,则系统可能能够根据其内容来计算该视图的合适大小。

堆栈视图就是一个很好的例子。除其他限制外,系统会根据其内容和属性来计算堆栈视图的大小。在许多方面,堆栈视图的行为就好像具有一个固有的内容大小:你可以仅使用一个垂直约束和一个水平约束来定义其位置来创建有效的布局。但是它的大小是由自动布局计算的,它不是自动布局的输入。设置堆栈视图的CHCR优先级没有任何作用,因为堆栈视图没有固有的内容大小。

如果你需要相对于堆栈视图外部的项目调整堆栈视图的适合大小,请创建显式约束以捕获这些关系,或者修改堆栈内容相对于堆栈外部项目的CHCR优先级。

3.10 值的含义

自动布局中的值始终以点(points)为单位。

但是,这些测量的确切含义会根据所涉及的属性和视图的布局方向而变化。


参考资料:

  1. Understanding Auto Layout
  2. Auto Layout Without Constraints
  3. Anatomy of a Constraint
  4. Programmatically Creating Constraints
  5. Auto Layout Cookbook
  6. Debugging Auto Layout