自动布局指南-Part 4:高级自动布局

翻译自“Auto Layout Guide”。

4 高级自动布局

4.1 通过代码创建约束

尽可能使用界面生成器设置约束。界面生成器提供了很多工具,可以可视化,编辑,管理和调试约束。通过分析约束,它还会在设计时发现很多常见的错误,让你在运行应用程序之前修复它们。

界面生成器可以管理日益增长的任务。可以在界面生成器中直接构建几乎任何类型的约束(查看“在界面生成器中使用约束”)。还可以指定具体尺寸类的约束(查看“调试自动布局”),以及使用新的工具,例如堆栈视图,甚至还可以在运行时动态添加或移除视图(查看”动态的堆栈视图“)。然而,视图层级结构的某些动态变化只能在代码中管理。

通过代码创建约束时,你有三个选择:使用布局锚点,使用NSLayoutConstraint类,或者使用Visual Format Language。

4.1.1 布局锚点(Anchors)

NSLayoutAnchor类为创建约束提供了连贯(fluent)的接口。通过访问要约束项的anchor属性使用该API。例如,视图控制到的顶部和底部布局向导有topAnchor,bottomAnchor和heightAnchor属性。另一方面,视图为边缘(edge),中心(center),尺寸(size)和基线(baseline)暴露了锚点。

提示
在iOS中,视图还有layoutMarginsGuide和readableContentGuide属性。这些属性暴露了UILayoutGuide对象,分别表示视图的页边留白和可读内容向导(readable content guides)。反过来,这些向导为边缘,中心和尺寸暴露了锚点。
通过代码创建约束到页边留白或可读内容向导时,使用这些向导。

布局锚点让你创建易读的,紧凑格式的约束。它们暴露一系列方法创建不同类型的约束,如列表13-1所示。

列表13-1 创建布局锚点

// Get the superview's layout
let margins = view.layoutMarginsGuide
 
// Pin the leading edge of myView to the margin's leading edge
myView.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor).active = true
 
// Pin the trailing edge of myView to the margin's trailing edge
myView.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor).active = true
 
// Give myView a 1:2 aspect ratio
myView.heightAnchor.constraintEqualToAnchor(myView.widthAnchor, multiplier: 2.0)

正如“Anatomy of a Constraint”中描述的,一个约束是一个简单的线性方程式。

自动布局指南-Part 4:高级自动布局_第1张图片

布局锚点有不同的方法创建约束。每个方法只包括影响结果的方程式元素。所以,下面的代码中:

myView.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor).active = true

符号相等于方程式的以下部分:

Equation Symbol
Item 1 myView
Attribute 1 leadingAnchor
Relationship constraintEqualToAnchor
Multiplier None (defaults to 1.0)
Item 2 margins
Attribute 2 leadingAnchor
Constant None (defaults to 0.0)

布局锚点还提供了额外的类型安全。NSLayoutAnchor类有很多子类,为创建约束添加了类型信息,以及具体子类的方法。它帮助阻止意外创建无效的约束。例如,可以只约束水平锚点(leadingAnchor或trailingAnchor)到其它水平锚点。类似的,可以只为尺寸约束提供乘数。

提示
这些规则不是NSLayoutConstraint API的强制要求。相反,如果创建了一个无效的约束,该约束会在运行时抛出异常。因此,布局锚点帮助把运行时错误转换为编译时的错误。

更多信息请参考“NSLayoutAnchor Class Reference”。

4.1.2 NSLayoutConstraint类

你还可以直接使用NSLayoutConstraint类的constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:便利(convenience)方法创建约束。该方法显式的把约束方程式转换为代码。每个参数对应方程式的一部分(参考“约束方程式”)。

不像布局锚点API提供的方法,该方法必须为每个参数指定值,即使它不影响布局。最终结果是相当多的引用代码(boilerplate code),该代码通常很难阅读。例如,列表13-2中的代码在功能上与列表13-1相同。

列表13-2 直接实例化约束

NSLayoutConstraint(item: myView, attribute: .Leading, relatedBy: .Equal, toItem: view, attribute: .LeadingMargin, multiplier: 1.0, constant: 0.0).active = true
 
NSLayoutConstraint(item: myView, attribute: .Trailing, relatedBy: .Equal, toItem: view, attribute: .TrailingMargin, multiplier: 1.0, constant: 0.0).active = true
 
NSLayoutConstraint(item: myView, attribute: .Height, relatedBy: .Equal, toItem: myView, attribute:.Width, multiplier: 2.0, constant:0.0).active = true

提示
在iOS中,NSLayoutAttribute枚举包括视图页边留白的值。这意味着你可以不通过layoutMarginsGuide属性,就能创建到页边留白的约束。但是,你仍然需要使用readableContentGuide属性,创建到可读内容向导的约束。

不像布局锚点API,便利方法不会高亮具体约束的重要特征。因此,阅读代码时,更容易错过重要的细节。另外,编译器不会执行约束的任何静态分析。你可以很自由的创建无效约束。然后这些约束在运行时抛出异常。因此,除非你需要支持iOS 8或者OS X v10.10,或者更早的版本,考虑迁移到更新的布局锚点API。

更多信息请参考“NSLayoutConstraint Class Reference”。

4.1.3 Visual Format Language

Visual Format Language让你可以使用ASCII艺术(ASCII-art),比如字符串,来定义约束。这提供了约束的视觉描述。Visual Formatting Language有以下优点和缺点:

  • 自动布局使用Visual Format Language在控制台打印约束;基于这个原因,调试信息看起来很像创建约束的代码。
  • Visual Format Language让你可以一次创建多个约束,通过使用一个简洁的表达式。
  • Visual Format Language只能创建有效的约束。
  • 符号强调良好可视化的完整性。因此,有些约束(例如长宽度)不能使用Visual Format Language创建。
  • 编译器不会以任何方式验证字符串。只能通过运行时测试发现错误。

使用Visual Format Language重写列表13-1中的例子:

let views = ["myView" : myView]
let formatString = "|-[myView]-|"
 
let constraints = NSLayoutConstraint.constraintsWithVisualFormat(formatString, options:.AlignAllTop , metrics: nil, views: views)
 
NSLayoutConstraint.activateConstraints(constraints)

该例子同时创建和启用开头和结尾约束。使用默认间隔时,Visual Format Language总是创建到父视图的页边留白10个点的约束,因此这些约束跟之前的例子相同。但是,列表13-3不能创建长宽比约束。

如果创建一行有多个项的复杂视图,Visual Format Language同时制定垂直对齐和水平间隔。从字面上看,“Align All Top”选项对布局没有影响,因为例子中只有一个视图(不包括父视图)。

使用Visual Format Language创建约束需要以下步骤:

  1. 创建views字典。该字典必须使用字符串作为key,视图对象(或者自动布局中可以被约束的其它项,例如布局向导)作为值。使用格式字符串识别视图。

提示
使用Objective-C时,使用NSDictionaryOfVariableBindings宏创建视图字典。在Swift中,必须自己创建字典。

  1. (可选)创建度量(metrics)字典。该字典必须使用字符串作为key,NSNumber对象作为值。使用kye表示格式化字符串的常量值。
  2. 通过布局项的单行或单列创建格式化字符串。
  3. 调用NSLayoutConstraint类的constraintsWithVisualFormat:options:metrics:views:方法。该方法返回包括所有约束的数组。
  4. 调用NSLayoutConstraint类的activateConstraints:方法启用约束。

更多信息请参考“Visual Format Language”附录。

4.2 具体尺寸类的布局

界面生成器的故事版默认使用尺寸类。尺寸类是分配给用户界面元素(比如场景或视图)的特征(traits)。它们大致表示元素的尺寸。界面生成器根据当前尺寸类,让你自定义很多布局特征。尺寸类变化时,布局自动适配。特别是,你可以在每个尺寸类基础上设置以下特征:

  • 安装或卸载一个视图或控件。
  • 安装或卸载一个约束。
  • 设置选中属性的值(例如,字体和布局页边留白设置)。

当系统加载场景时,它实例化所有视图,控件和约束,并在视图控制器中指定这些项的合适outlet(如果有的话)。不管场景的当前尺寸类,你都可以通过项的outlet访问它们。但是,只有在项被安装在当前尺寸类时,系统才会添加它们到视图层级结构。

当视图的尺寸类改变时(例如,旋转iPhone,或者在全屏和分割视图中切换iPad应用程序),系统自动添加项到视图层级结构,或者从视图层级结构中移除它们。系统也会动画的改变视图的布局。

提示
系统保持卸载项的引用,所以当它们从视图层级结构中移除时,它们没有被释放。

4.2.1 最终(Final)和基础(Base)尺寸类

界面生成器识别九个不同的尺寸类。

其中四个是最终尺寸类:Compact-Compact,Compact-Regular,Regular-Compact和Regular-Regular。最终尺寸类表示在设备上显示的实际尺寸类。

其余五个是基础尺寸类:Compact-Any,Regular-Any,Any-Compact,Any-Regular和Any-Any。这些抽象尺寸类表示两个或多个最终尺寸类。例如,安装在Compact-Any尺寸类中的项,出现在Compact-Compact和Compact-Regular尺寸视图中。

在更具体尺寸类中的设置会覆盖更通用的尺寸类。另外,你必须为所有九个尺寸类提供没有歧义的,可满足的布局,包括基础尺寸类。因此,最简单的方法是从最通用的尺寸类到最具体的尺寸类。选择应用程序的默认布局,并在Any-Any尺寸类中设计该布局。然后根据需要修改基础或最终尺寸类。

4.2.2 使用尺寸类工具

使用界面生成器的尺寸类工具选择当前编辑的尺寸类。该工具在编辑创建的顶部中心。默认情况下,界面生成器选择Any-Any尺寸类。

自动布局指南-Part 4:高级自动布局_第2张图片

点击尺寸类工具切换到新的尺寸类。界面生成器弹出一个包括3 × 3网格尺寸类的弹出框视图。在网格中移动鼠标,改变尺寸类。网格在顶部显示选中的尺寸类名称,在底部显示尺寸类的描述(包括它影响的设备和方向)。它还在当前尺寸类影响的每一个尺寸类中显示一个绿色的原点。

自动布局指南-Part 4:高级自动布局_第3张图片

添加到画布的任何视图或约束只安装在当前尺寸类。当删除项时,行为根据在哪和如何删除项变化。

  • 从画布或者文档大纲中删除项,会从整个工程中移除。
  • 从画布或者文档大纲中Command-Deleting项,只会从当前尺寸类中卸载项。
  • 当场景有多个尺寸类时,从画布或文档大纲之外的任何地方(例如,从尺寸检查器中选中并删除约束)删除项,只会从当前尺寸类终卸载项。
  • 如果只在Any-Any尺寸类中编辑,删除项总是从项目中移除它。

如果你正在编辑Any-Any之外的任何尺寸类,界面生成器在编辑器底部蓝色高亮显示工具栏。

4.2.3 使用检查器

你还可以在检查器中修改具体尺寸类的设置。任何支持具体尺寸类的设置,都在检查器中带一个小的加号图标显示。

默认情况下,检查器为Any-Any尺寸类设置值。点击加号图标添加一个新的尺寸类,来设置更具体尺寸类的值。为你想添加的尺寸类选择宽度,然后选择高度。

自动布局指南-Part 4:高级自动布局_第4张图片

现在,检查器在它自己的行显示每一个尺寸类——Any-Any设置在第一行,更具体的尺寸类在下面列出。你可以单独编辑每一行的值。

自动布局指南-Part 4:高级自动布局_第5张图片

点击行开头的x图标移除一个自定义尺寸类。

在界面生成器中使用尺寸类的更多信息,请参考“Size Classes Design Help”。

4.3 使用滚动视图

使用滚动视图时,你需要同时定义滚动视图在它父视图内的frame的尺寸和位置,以及滚动视图内容区域的尺寸。所有这些特征都可以使用自动布局设置。

为了支持滚动视图,系统根据约束放置的不同地方,来解释约束。

  • 滚动视图和滚动视图之外的对象之间的任何约束,连接到滚动视图的frame,跟其它任何视图一样。
  • 滚动视图和它内容之间的约束,行为根据被约束的属性改变:
  • 滚动视图的边缘(edge)或页边留白(margin)和它内容之间的约束,连接到滚动视图的内容区域。
  • 高度,宽度或中心之间的约束,连接到滚动视图的frame。
  • 还可以使用滚动视图的内容和滚动视图之外的对象之间的约束,为滚动视图的内容提供一个固定位置,让内容悬浮在滚动视图中。

对于最普遍的布局任务,如果使用虚拟视图,或者分组布局滚动视图的内容,逻辑会变得更简单。使用界面生成器时,通用方法如下:

  1. 添加滚动视图到场景中。
  2. 跟通常一样,绘制约束定义滚动视图的尺寸和位置。
  3. 添加视图到滚动视图中。设置视图的Xcode specific label为Content View。
  4. 固定内容视图的顶部,底部,开头和结尾边缘到滚动视图相应的边缘。现在,内容视图定义了滚动视图的内容区域。

记住
此时内容视图没有固定的尺寸。它可以拉伸和增大来适应你放置在里面的任何视图和控件。

  1. (可选)设置内容视图的宽度等于滚动视图的宽度,来禁用水平滚动。现在,内容视图水平填充滚动视图。
  2. (可选)设置内容视图的高度等于滚动视图的高度,来禁用垂直滚动。现在,内容视图垂直填充滚动视图。
  3. 在内容视图中布局滚动视图的内容。跟通常一样,使用约束在内容视图中放置内容。

重要
你的布局必须完全定义内容视图额尺寸(除了步骤5和6中定义的)。要想根据内容的固有尺寸设置高度,必须有一个完整的约束链,以及从内容视图的顶部边缘到底部边缘的视图拉伸。类似的,要想设置宽度,必须有一个完整的约束链,以及从内容视图的开头边缘到结尾边缘的视图拉伸。
如果内容没有固有内容尺寸,则必须添加适当的尺寸约束到内容视图或者到内容。
当内容视图比滚动视图高时,滚动视图启用垂直滚动。当内容视图比滚动视图宽时,滚动视图启用水平滚动。否则,默认情况下禁用滚动。

4.4 使用自我调整尺寸的表格视图单元格

在iOS中,可以使用自动布局定义表格视图单元格的高度;但是,该特征默认是禁用的。

通常,单元格的高度由表格视图代理的tableView:heightForRowAtIndexPath:方法决定。要启用自我调整大小的表格视图单元格,必须设置表格视图的rowHeight属性为UITableViewAutomaticDimension。还必须给estimatedRowHeight属性分配一个值。当这两个属性都设置后,自动使用自动布局计算行的实际高度。

tableView.estimatedRowHeight = 85.0
tableView.rowHeight = UITableViewAutomaticDimension

接下来,在单元格的内容视图中布局表格视图单元格的内容。要想定义单元格的高度,需要一个完整的约束链和视图(已经定义了高度)填充内容视图的顶部边缘和底部边缘之间的区域。如果视图有固有内容高度,系统使用这些值。如果没有,你必须添加适当的高度约束到视图或者内容视图本身。

自动布局指南-Part 4:高级自动布局_第6张图片

另外,尝试让预估的行高尽可能准确。系统根据这些预估值计算项(比如滚动栏)的高度。预估值越精确,用户体检更天衣无缝。

提示
使用表格视图单元格时,你不能改变预定义内容的布局(例如,textLabel,detailTextLabel和imageView属性)。
支持以下约束:

  • 相对于单元格的内容视图约束子视图的位置。
  • 相对于单元格的bounds约束子视图的位置。
  • 相对于预定义内容约束子视图的位置。

4.5 改变约束

一个约束的改变是改变底层约束的数学表达式(如图17-1)。可以在“Anatomy of a Constraint”中学习更多约束方程式。

图17-1 约束方程式

自动布局指南-Part 4:高级自动布局_第7张图片

以下所有动作都会改变一个或多个约束:

  • 启用或禁止一个约束。
  • 改变约束的常量值。
  • 改变约束的优先级。
  • 从视图层级结构中移除一个视图。

其它改变,比如设置控件的属性,或者修改视图层级结构,也会改变约束。当改变发生时,系统调度一个推迟的布局过程(deferred layout pass)。

通常,你可以在任何时候做出这些改变。理想情况是,大部分约束在界面生成器中设置,或者通过代码,在视图控制器的初始化设置中创建(例如,在viewDidLoad中)。如果需要在运行时动态改变约束,最好在应用程序状态变化是改变它们。例如,你想在按钮点击时改变约束,直接在按钮的动作方法中做出改变。

你可能偶尔因为性能原因,需要批量改变。更多信息请参考“批量改变”。

4.5.1 推迟的布局过程

自动布局为不久的将来调度一个布局过程,而不是立即更新受影响视图的frame。该推迟的过程更新布局的约束,然后为视图层级结构的所有视图计算frame。

可以通过调用setNeedsLayout方法或者setNeedsUpdateConstraints方法,调度自己的推迟的布局过程。

推迟的布局过程实际涉及视图层级结构的两个过程:

  1. 更新过程根据需要更新约束。
  2. 布局过程根据需要重新定位视图的frame。

4.5.1.1 更新过程

系统遍历视图层级结构,并在所有视图控制器上调用updateViewConstraints方法,在所有视图上调用updateConstraints方法。你可以覆写这些方法,来优化约束的改变(查看“批量改变”)。

4.5.1.2 布局过程

系统再次遍历视图层级结构,并在所有视图控制器上调用viewWillLayoutSubviews方法,在所有视图上调用layoutSubviews(在OS X上layout)。默认情况下,layoutSubviews方法使用自动布局引擎计算的矩形更新每个子视图的frame。你可以覆写这些方法来修改布局(查看“自定义布局”)。

4.5.2 批量改变

影响变化发生后,立即更新约束几乎总是更干净和容易。推迟这些改变到一个之后的方法会让代码复杂,更难理解。

然而,有些时候你可能基于性能原因,希望批量修改。只有在就地改变约束太慢,或者当视图做了很多多余的改变时,才应该这么做。

要想批量改变,在持有约束的视图上调用setNeedsUpdateConstraints方法,而不是直接做出改变。然后,覆写视图的updateConstraints方法,来修改受影响的约束。

提示
你的updateConstraints实现必须尽可能高效。不要禁用所有约束,然后启用你需要的。相反,你的应用程序必须有些方式来追踪你的约束,并在每个更新过程中验证它们。只有变化的项需要改变。在每一个更新过程中,你必须确保应用程序的当前状态有合适的约束。

总是在你实现的updateConstraints方法的最后一步调用父类的实现。

不要在你的updateConstraints方法中调用setNeedsUpdateConstraints。调用setNeedsUpdateConstraints调度另一个更新过程,创建了一个反馈回路(feedback loop)。

4.5.3 自定义布局

覆写viewWillLayoutSubviews或layoutSubviews方法来修改布局引擎返回的结果。

重要
如果可能,使用约束定义所有布局。结果布局更健壮和更容易调试。当你需要创建的布局不能只使用约束表示时,你应该只覆写viewWillLayoutSubviews或layoutSubviews方法。

覆写这些方法时,布局在一个不一致的状态。有些视图已经布局好了,其它的还没有。你需要十分小心如何修改视图层级结构,否则会创建反馈回路。以下规则帮助你避免反馈回路:

  • 必须在你的方法中某些地方调用父类的实现。
  • 你可以安全的在你的子树(subtree)中让视图的布局无效;但是,必须在调用父类的实现之前。
  • 不要在你的子树之外让任何视图的布局无效。这会创建一个反馈回路。
  • 不要调用setNeedsUpdateConstraints。你刚完成一个布局过程。调用该方法会创建一个反馈回路。
  • 不要调用setNeedsLayout。调用该方法会创建一个反馈回路。
  • 小心的改变约束。你不想在子树之外让任何视图的布局意外的无效。

你可能感兴趣的:(自动布局指南-Part 4:高级自动布局)