Vincent Sit

iOS 开发实践之 Auto Layout

前言
iOS 开发实践之 Auto Layout

2018.09.21 更新: 修复了死链。
距离本文最初发布已经过去 4 年多了,Auto Layout 发生了一些变化,许多重要的特性被引入,比以前更加强大了。但这篇文章依然没有过时,如果你希望学习并理解 Auto Layout,希望这篇文章能够帮到你。

2015.11.11 更新: 由于 Masonry 强大的特性,建议大家优先考虑使用它。

本文是博主 iOS 开发实践系列中的一篇,主要讲述 iOS 中 Auto Layout(自动布局)在实际项目中的使用。

Auto Layout 在 2012 年的 iOS 6 中发布,距今已经 2 年多了,如果从 2011 年在 Mac OS X 上发布的 Auto Layout 开始算起,已经超过 3 年了。如果你的简历上写着 2 年以上工作经验,而竟然不会使用 Auto Layout,真有点不可思议。

本文将会通过若干个 Demo 进行讲解,通过实践来理解 Auto Layout 到底是什么,该如何使用(包括在 Xib 中使用以及手动编码)。

Auto Layout 是什么?

我的理解:Auto Layout 是一种基于约束的布局系统,它可以根据你在元素(对象)上设置的约束自动调整元素(对象)的位置和大小。

官方的说明:

Auto Layout 是一个系统,可以让你通过创建元素之间关系的数学描述来布局应用程序的用户界面。——《Auto Layout Guide

Auto Layout 是一种基于约束的,描述性的布局系统。——《Taking Control of Auto Layout in Xcode 5 - WWDC 2013

这里有几个关键字:

  • 元素
  • 关系
  • 约束
  • 描述

元素(Element)

低头看看你电脑的键盘,你可以把每一个按键当做一个元素;对于 iOS 系统来说,你可以把桌面上每一个应用图标当做一个元素;对于某一款 iOS 应用来说,你可以把视图中的每一个子视图当做一个元素。

事实上,你也可以把整个键盘、桌面或者视图当做一个元素。

关系(Relation)

元素之间可以有关系。例如在键盘上 Q 键和 W 键之间有关系。是什么关系呢?有很多,例如 Q 键在 W 键的左边,W 键在 Q 键的右边,Q 键和 W 键之间相距 0.5 厘米等等。

不理解?试着把键盘想象成 View,把按键想象成 Button,再思考一遍。

约束(Constraint)

元素之间关系的限制。约束是 Auto Layout 系统中最重要的概念。我们上面提到的 左边右边 以及 相距 0.5 厘米 等这些都是约束,它们限制了元素之间的关系。

描述(Description)

定义约束来限制元素之间的关系。描述定义了元素之间的关系及约束。

继续用键盘举例,**Q 键的长宽均为 1 厘米,左边距离键盘的左边缘 10 厘米,上边距离键盘的顶部 5 厘米。**这句话就可以定位 Q 键在键盘中的位置,很轻松就可以计算出 Q 键的 frame{{10.0, 5.0}, {1.0, 1.0}}

现在 Q 键的坐标已经确定,那么 W 键的坐标可以这样描述:**顶部和 Q 键对齐,大小和 Q 键相等,位于 Q 键右侧 0.5 厘米处。**仔细想想,这句话中包含了元素间的关系,关系间的约束,可以直接计算出 W 键的 frame

忘掉传统的 Springs & Struts 布局方式

事实上如果你用传统的设置 frame 的布局方式的思维来理解上面的 Q 键和 W 键的布局也说的通。

因为在 Auto Layout 中,当你描述完之后, Auto Layout 会自动帮你计算出 frame。换句话说,你的描述告诉了 Auto Layout 如何帮你计算出 frame。所以,你也可以理解为你间接的设置了 frame。为什么要这么做呢?为什么不直接设置 frame?这是因为使用 Auto Layout 有很多好处:

  • 多数情况下旋转屏幕不用再做额外的处理
  • 更容易适配不同尺寸的屏幕
  • 上手后布局非常简单容易,布局逻辑更清晰

Auto Layout 和传统布局很大的不同之处在于它是一种相对的布局方式。怎么理解这句话?上面提到

W 键位于 Q 键右侧 0.5 厘米处。

传统的布局无法直接表示,你必须把这种布局手动转换为传统布局代码。例如上面的 Q 键和 W 键的传统布局代码看起来可能是这样:

q.frame = CGRectMake(CGRectGetMinX(keyBoard.frame) + 10.f, CGRectGetMinY(keyBoard.frame) + 5.f, 1.f, 1.f);
w.frame = CGRectMake(CGRectGetMaxX(q.frame) + 0.5f, CGRectGetMinY(q.frame), CGRectGetWidth(q.frame), CGRectGetHeight(q.frame));

使用 Auto Layout 的布局代码看起来像这样:

// 伪代码
q.width = 1.f;
q.height = 1.f;
q.left = keyboard.left + 10.f;
q.top = keyboard.top + 5.f;

w.top = q.top;
w.width = q.width;
w.height = q.height;
w.left = q.right + .5f;

Auto Layout 不仅能轻松表示这种布局,而且相对于传统的布局更清晰简洁易懂,还免费附赠很多优点,有什么理由不使用 Auto Layout 呢?

实践中我发现对于很多新手来说,Auto Layout 这种布局方式比较容易理解接受,相反很多对传统布局很熟练的人却不太容易理解,总是用传统布局的思维来思考,所以如果可能的话,我建议你暂时忘掉传统的布局方式。

Autoresizing Mask

事实上我不打算讲这个东西,以及它和 Auto Layout 的区别和联系。如果你不知道,对学习 Auto Layout 不会有什么影响。

你唯一需要注意的是在使用 Auto Layout 时,首先需要将视图的 translatesAutoresizingMaskIntoConstraints 属性设置为 NO。这个属性默认为 YES,如果你是使用 Xib 的话,这个属性会自动帮你设置为 NO。当它为 YES 时,运行时系统会自动将 Autoresizing Mask 转换为 Auto Layout 的约束,这些约束很有可能会和我们自己添加的产生冲突。

Auto Layout 基础知识

无论是在 Xib 中还是代码中使用 Auto Layout,你都需要了解 Auto Layout 的一些必要知识。这些你现在不理解没有关系,后面我们会详细讲述。

约束 (Constraint)

Auto Layout 中约束对应的类为 NSLayoutConstraint,一个 NSLayoutConstraint 实例代表一条约束。

NSLayoutConstraint 有两个方法,第一个是

+ (id)constraintWithItem:(id)view1
               attribute:(NSLayoutAttribute)attribute1
               relatedBy:(NSLayoutRelation)relation
                  toItem:(id)view2
               attribute:(NSLayoutAttribute)attribute2
              multiplier:(CGFloat)multiplier
                constant:(CGFloat)constant;

不要被这个方法的参数吓到,实际上它只做一件事,就是让 view1 的某个 attribute 等于 view2 的某个 attributemultiplier 倍加上 constant

这里的 attribute可以是上下左右宽高等等。

精简后就是下面这个公式:

view1.attribute1 = view2.attribute2 × multiplier + constant

还有一个参数是 relation,这是一个关系参数,它标明了上面这个公式两边的关系,它可以是小于等于 (≤)等于 (=)大于等于 (≥)。上面的公式假定了这个参数传入的是 =,根据参数的不同,公式中的关系符号也不同。

需要注意的是, 优先会使用 = 关系,如果 = 不能满足,才会使用 <>。例如设置一个 ≥ 100 的关系,默认会是 100,当视图被拉伸时,100 无法被满足,尺寸才会变得更大。

例子:

1、我们要实现一个如下图的布局。

布局代码如下:

UIView *view = [UIView new];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];

CGRect viewFrame = CGRectMake(50.f, 100.f, 150.f, 150.f);

// 使用 Auto Layout 布局
[view setTranslatesAutoresizingMaskIntoConstraints:NO];

// `view` 的左边距离 `self.view` 的左边 50 点.
NSLayoutConstraint *viewLeft = [NSLayoutConstraint constraintWithItem:view
                                                            attribute:NSLayoutAttributeLeading
                                                            relatedBy:NSLayoutRelationEqual
                                                               toItem:self.view
                                                            attribute:NSLayoutAttributeLeading
                                                           multiplier:1
                                                             constant:CGRectGetMinX(viewFrame)];
// `view` 的顶部距离 `self.view` 的顶部 100 点.
NSLayoutConstraint *viewTop = [NSLayoutConstraint constraintWithItem:view
                                                           attribute:NSLayoutAttributeTop
                                                           relatedBy:NSLayoutRelationEqual
                                                              toItem:self.view
                                                           attribute:NSLayoutAttributeTop
                                                          multiplier:1
                                                            constant:CGRectGetMinY(viewFrame)];
// `view` 的宽度 是 60 点.
NSLayoutConstraint *viewWidth = [NSLayoutConstraint constraintWithItem:view
                                                             attribute:NSLayoutAttributeWidth
                                                             relatedBy:NSLayoutRelationGreaterThanOrEqual
                                                                toItem:nil
                                                             attribute:NSLayoutAttributeNotAnAttribute
                                                            multiplier:1
                                                              constant:CGRectGetWidth(viewFrame)];
// `view` 的高度是 60 点.
NSLayoutConstraint *viewHeight = [NSLayoutConstraint constraintWithItem:view
                                                              attribute:NSLayoutAttributeHeight
                                                              relatedBy:NSLayoutRelationGreaterThanOrEqual
                                                                 toItem:nil
                                                              attribute:NSLayoutAttributeNotAnAttribute
                                                             multiplier:1
                                                               constant:CGRectGetHeight(viewFrame)];
// 把约束添加到父视图上.
[self.view addConstraints:@[viewLeft, viewTop, viewWidth, viewHeight]];

实现一个如此简单的布局竟然要写这么多的代码,这显然难于推广使用。于是 UIKit 团队发明了另外一种更简便的表达方式进行布局,这个我们后面再讲,现在先看看这段代码。

首先我把 viewtranslatesAutoresizingMaskIntoConstraints 设为了 NO,禁止将 Autoresizing Mask 转换为约束。

然后在设置 viewLeft 这个约束时,attribute 参数使用了 NSLayoutAttributeLeading 而不是 NSLayoutAttributeLeft,这两个参数值都表示左边,但它们之间的区别在于 NSLayoutAttributeLeft 永远表示左边,但 NSLayoutAttributeLeading 是根据习惯区分的,例如在某些文字从右向左阅读的地区,例如阿拉伯,NSLayoutAttributeLeading 表示右边。换句话说,NSLayoutAttributeLeading 是表示文字开始的方向。在英文、中文这种从左往右阅读的文字中它表示左边,在像阿拉伯语、希伯来语这种从右往左阅读的文字中它表示右边。通常情况下,除非你明确要限制在左边,否则你都应该使用 NSLayoutAttributeLeading 表示左边。相对的,表示右边也类似这样。这对于我们的本地化工作有很大的帮助。

然后在设置 viewWidthviewHeight 这两个约束时,relatedBy 参数使用的是 NSLayoutRelationGreaterThanOrEqual 而不是 NSLayoutRelationEqual

因为 Auto Layout 是相对布局,所以通常你不应该直接设置宽度和高度这种固定不变的值,除非你很确定视图的宽度或高度需要保持不变。

如果一定要设置高度或宽度,特别是宽度,在没有显式地设置内容压缩优先级(Content Hugging Priority,后面会讲到)和内容抗压缩优先级(Content Compression Resistance Priority,后面会讲到)的情况下,尽量不要使用 NSLayoutRelationEqual 这种绝对的关系,这会带来许多潜在的问题:

  • 根据内容决定宽度的视图,当内容改变时,外观尺寸无法做出正确的改变
  • 在本地化时过长的文字无法显示,造成文字切断,或文字过短,宽度显得过宽,影响美观
  • 添加了多余的约束时,约束之间冲突,无法显示正确的布局

所带来的问题不仅仅局限与这几条,这里只是简单列出几条。

如何正确的设置宽度或高度?给出一些 Tips:

  • 如果宽度和高度布局可以改变,使用固有内容尺寸(Intrinsic Content Size,后面会讲到)设置约束(即 size to fit size)。
  • 如果宽度和高度布局不可以改变,改变约束的关系为
  • 调整压缩优先级和内容抗压缩优先级

最后我把所有约束都添加到了 view 的父视图 self.view 上。view 的约束为什么不添加到自身而添加到别的视图上去呢?这是由于约束是根据视图层级自下而上更新的,也就是从子视图到父视图。所以 Auto Layout 添加约束有一套自己的规则,如下:

  • 两个同层级间视图的约束,添加到它们共同的父视图上

  • 两个不同层级间视图的约束,添加到它们最近的共同的父视图上

  • 两个有层级关系的视图的约束,添加到层次较高的视图上(父视图)上

因为我们属于最后一种情况,所以子视图 view 的约束添加到了父视图 self.view 上。

接下来是第二个方法

+ (NSArray *)constraintsWithVisualFormat:(NSString *)format 
                                 options:(NSLayoutFormatOptions)opts 
                                 metrics:(NSDictionary *)metrics 
                                   views:(NSDictionary *)views;

这个方法是我们实际编程中最常用的方法。它会根据我们指定的参数返回一组约束。
这个方法很重要,所以我会详细解释每个参数的用途。

format

这个参数存放的是布局逻辑,布局逻辑是使用 可视化格式语言 (VFL) 编写的。实际编程中我们也是使用 VFL 编写布局逻辑,因为第一个方法明显参数过多,一个简单的布局要写很多代码。

上一个布局使用 VFL 来重构的话,代码如下:

....
[view setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];

哗,代码量减少了很多。首先我们使用 NSDictionaryOfVariableBindings(...) 宏创建了一个字典 views,这个宏会自动把传入的对象的键路径作为字典的键,把对象作为字典的值。所以 views 字典的内容就像这样:

{@"self.view": self.view, @"view", view}

VFL 就是这两句:

H:|-50-[view(>=150)]

V:|-100-[view(>=150)]

第一句是在水平方向布局,表示 view 左边距离父视图左边 50 点,宽度至少 150 点。(水平方向是宽度)

第二句是在垂直方向上布局,表示 view 顶部距离父视图顶部 100 点,宽度至少 150 点。(垂直方向是高度)

分解说明如下:

H / V 表示布局方向。H 表示水平方向(Horizontal),V 表示垂直方向(Vertical),方向后要紧跟一个 :,不能有空格。

| 表示父视图。通常出现在语句的首尾。

- 有两个用途,单独一个表示标准距离。这个值通常是 8 ;两个中间夹着数值,表示使用中间的数值代替标准距离,如第一句的 -50-,就是使用 50 来代替标准距离。

[] 表示对象,括号中间需要填上对象名,对象名必须是我们传入的 views 字典中的键。对象名后可以跟小括号 (),小括号中是对此对象的尺寸和优先级约束。水平布局中尺寸是宽度,垂直布局中尺寸是高度。如第一句中的 (>=150) 就是对 view 尺寸的约束,因为是水平方向布局,所以它表示宽度大于或等于 150 点。而 150 前面的 >= 就是我们上面第一个方法中提到的关系参数。至于为什么这里使用 >=,上面已经解释过了。括号中可以包含多条约束,如果我们想再加一条约束,保证 view 的宽度最大不超过 200 点,我们可以这样写:H:|-50-[view(>=150,<=200)]。还可以添加优先级约束,这个我们后面再讲。

VFL 语法有几点需要注意:

  • 布局语句中不能包含空格
  • 和关系一样,没有 >< 这种约束

然后下面是一些例子,增加你对 VFL 语法的理解。

例一:

我们在 view 右侧添加另一个视图 view2,效果如图:

代码如下:

UIView *view = [UIView new];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];

UIView *view2 = [UIView new];
[view2 setBackgroundColor:[UIColor blueColor]];
[self.view addSubview:view2];

[view setTranslatesAutoresizingMaskIntoConstraints:NO];
[view2 setTranslatesAutoresizingMaskIntoConstraints:NO];

NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[view]-[view2(>=50)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view2(>=50)]" options:0 metrics:nil views:views]];

我们讲讲最后的两条新的 VFL 语句:

H:[view]-[view2(>=50)]

从开始的 H: 我们可以判断出这是水平方向的布局,换句话说就是设置视图的 xwidth。接着的 [view],说明后面的所有视图都是在 view 的右侧;接着是 -,说明后一个视图和 view 之间有一个标准距离的间距;也就是说 x 等于 view 的右侧再加上标准距离,即 CGRectGetMaxX(view) + 标准距离。最后是 [view2(>=50)],这里可以看出后一个视图是 view2,并且它的宽度不小于 50 点。整一句翻译成白话就是说:在水平方向上,view2view 右侧的标准距离位置处,并且它的宽度不小于 50 点。

V:|-100-[view2(>=50)]

从开始的 V: 我们可以判断出这是垂直方向的布局,换句话说就是设置视图的 yheight。接着的 | 说明是后一个视图是相对于父视图进行布局;接着是 -100-,说明垂直方向和父视图(顶部)相距 100 点,也就是说 y 等于 100 点。最后是 [view2(>=50)],这和上一句相同,只是因为是垂直方向,所以 50 是设置高度而不是宽度。整一句翻译成白话就是说:在垂直方向上,view2 在相对于父视图(顶部) 100 点的位置处,并且它的高度不小于 50 点。

实际上我们的代码还可以简化:

......
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]-[view2(>=50)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view2(>=50)]" options:0 metrics:nil views:views]];

因为两个视图水平方向上是并排(从左到右)的,所以我们可以将水平方向布局的代码合并到一起。而垂直方向我们并非并排的,所以垂直方向的布局代码我们不能合并。这里所讲的并排的意思是后一个在前一个的后面,水平方向上明显是这样,但垂直方向上两个视图的 y 是相同的,所以无法合并在一起布局。

例二:我们继续添加一个视图 view3 填补 view 右下方的空缺,效果如图:

代码如下:

UIView *view = [UIView new];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];

UIView *view2 = [UIView new];
[view2 setBackgroundColor:[UIColor blueColor]];
[self.view addSubview:view2];

UIView *view3 = [UIView new];
[view3 setBackgroundColor:[UIColor orangeColor]];
[self.view addSubview:view3];

[view setTranslatesAutoresizingMaskIntoConstraints:NO];
[view2 setTranslatesAutoresizingMaskIntoConstraints:NO];
[view3 setTranslatesAutoresizingMaskIntoConstraints:NO];

NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2, view3);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[view(>=150)]-[view2(>=50)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(100)-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[view]-[view3(>=50)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(100)-[view2(>=50)][view3(>=100)]" options:0 metrics:nil views:views]];

你可能注意到我把每个间距都使用小括号阔了起来,这是可选的,你完全可以直接写间距,这么写只是告诉你还有这种语法。实际上没什么必要这么写,因为 VFL 语法并不支持运算,例如把 (50) 切分为 (10+40)(5*10) 都是不合法的。

最后两行是 view3 的布局代码,简单解释一下:

H:[view]-[view3(>=50)]

水平方向布局,view3view 右侧标准距离处,并且宽度不小于 50 点。

V:|-(100)-[view2(>=50)][view3(>=100)]

垂直方向布局,view2 距离父视图(顶部)100 点,并且高度不小于 50 点;view3 紧挨着 view2 底部(没有 -),并且高度不小于 100 点。

options

这个参数的值是位掩码,使用频率并不高,但非常有用。它可以操作在 VFL 语句中的所有对象的某一个属性或方向。例如上面的例一,水平方向有两个视图,它们的垂直方向到顶部的距离相同,或者说顶部对齐,我们就可以给这个参数传入 NSLayoutFormatAlignAllTop 让它们顶部对齐,这样以来只需要指定两个视图的其中一个的垂直方向到顶部的距离就可以了。代码:

......
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]-[view2(>=50)]" options:NSLayoutFormatAlignAllTop metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[view2(>=50)]" options:0 metrics:nil views:views]];

它的默认值是 NSLayoutFormatDirectionLeadingToTrailing,根据当前用户的语言环境进行设置,比如英文中就是从左到右,希伯来语中就是从右到左。

这个值符合我们常用的选项。NSLayoutFormatDirectionLeadingToTrailing 的值是 0 << 16,所以我们可以直接传入 0 使用此值。

因为是位掩码,所以我们可以使用 | 进行多选,例如例一,我们希望在现有约束的基础上让两个视图的高度相等,那代码可以这样写:

......
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]-[view2(>=50)]" options:NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];

指定两个视图的顶部和底部约束相同,然后只设置其中一个视图的相关约束即可。

灵活使用此参数可以节省不少时间,但这个参数内容太多,如果你有兴趣了解,可以看看我的另一篇博文:《Auto Layout 中的排列选项》

metrics

这是一个字典,字典的键必须是出现在 VFL 语句中的字符串,值必须是 NSNumber 类型,作用是将在 VFL 语句中出现的键替换为相应的值。例如本文中的第一个布局的例子,使用了这个参数后代码就变成了这样:

UIView *view = [UIView new];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];

[view setTranslatesAutoresizingMaskIntoConstraints:NO];

CGRect viewFrame = CGRectMake(50.f, 100.f, 150.f, 150.f);

NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view);

NSDictionary *metrics = @{@"left": @(CGRectGetMinX(viewFrame)),
                          @"top": @(CGRectGetMinY(viewFrame)),
                          @"width": @(CGRectGetWidth(viewFrame)),
                          @"height": @(CGRectGetHeight(viewFrame))};

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view(>=height)]" options:0 metrics:metrics views:views]];

聪明的你看了这段代码后肯定已经明白这个参数的用途了,虽然使用频率不高,但依然很有用,特别是要动态计算约束值的时候非常有用。

实际上这个参数也可以使用 NSDictionaryOfVariableBindings(...) 宏来快速创建,代码如下:

......
[view setTranslatesAutoresizingMaskIntoConstraints:NO];

NSNumber *left = @50.f;
NSNumber *top = @100.f;
NSNumber *width = @150.f;
NSNumber *height = @150.f;

NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view);
NSDictionary *metrics = NSDictionaryOfVariableBindings(left, top, width, height);

[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view(>=height)]" options:0 metrics:metrics views:views]];

views

又是一个字典,包含了 VFL 语句中用到的视图。字典的键必须是出现在 VFL 语句中的视图名称,值必须视图的实例。这个字典我们在讲 format 时已经讲过,也用过很多次,相信你早已明白是怎么回事了。

讲了这么多,可能你也发现了,只要学会了 VFL 语法,就可以方便地使用 Auto Layout 了,其他的知识都属于辅助选项,会的话,布局更轻松一些,不会也没关系,实践多了,自然就会了。

优先级 (Priority level)

约束条件有优先级,高优先级约束会比低优先级约束优先得到满足,系统内置了 4 个优先级:

enum {
  UILayoutPriorityRequired = 1000,
  UILayoutPriorityDefaultHigh = 750,
  UILayoutPriorityDefaultLow = 250,
  UILayoutPriorityFittingSizeLevel = 50,
};
typedef float UILayoutPriority;
  • UILayoutPriorityRequired 这是默认值,这意味着这个约束条件必须被精确地满足。
  • UILayoutPriorityDefaultHigh
  • UILayoutPriorityDefaultLow
  • UILayoutPriorityFittingSizeLevel 这是内置的最低优先级。

相信你已经看到每个等级的数值了,优先级的取值在 0 ~ 1000 之间,取值越大,优先级越高,越会被优先满足。

每个约束的默认优先级就是 UILayoutPriorityRequired,这意味着你给出的所有约束都必须得到满足,一旦约束间发生冲突,你的应用就会 Crash。这也是在使用 Auto Layout 时经常会犯的错误:没有给约束设置适当的优先级。

举个例子说明优先级设置不当的情况,给我们首次使用 Auto Layout 时的例子再添加一个约束:

......

// `view` 的高度是 60 点.
NSLayoutConstraint *viewHeight = [NSLayoutConstraint constraintWithItem:view
                                                              attribute:NSLayoutAttributeHeight
                                                              relatedBy:NSLayoutRelationGreaterThanOrEqual
                                                                 toItem:nil
                                                              attribute:NSLayoutAttributeNotAnAttribute
                                                             multiplier:1
                                                               constant:CGRectGetHeight(viewFrame)];
// `view` 紧贴着 `self.view` 的左边.
NSLayoutConstraint *marginLeft = [NSLayoutConstraint constraintWithItem:view
                                                              attribute:NSLayoutAttributeLeading
                                                              relatedBy:NSLayoutRelationEqual
                                                                 toItem:self.view
                                                              attribute:NSLayoutAttributeLeading
                                                             multiplier:1
                                                               constant:0];

// 把约束添加到父视图上.
[self.view addConstraints:@[viewLeft, viewTop, viewWidth, viewHeight, marginLeft]];

运行看看效果,程序 Crash 了!控制台 Log 中有这么一段信息:

"<NSLayoutConstraint:0xXXXXXXX H:|-(50)-[UIView:0xXXXXXX]   (Names: '|':UIView:0xXXXXXX )>",
"<NSLayoutConstraint:0xXXXXXXX H:|-(0)-[UIView:0xXXXXXX]   (Names: '|':UIView:0xXXXXXX )>"

可以看到第一条是 viewLeft 这个约束,它限制了 view 的左边距离父视图的左边 50 点。

第二条是新添加的 marginLeft 这个约束,它限制了 view 的左边距离父视图的左边 0 点,也就是紧贴着父视图的左边。

很明显这两个约束是冲突的,当系统尝试根据优先级进行布局时,发现它们的优先级也相同,无法满足两个冲突的约束,所以抛出了异常。

我们只需要给两个约束设置不同的优先级即可解决。添加下面一行代码:

[viewLeft setPriority:UILayoutPriorityDefaultHigh];

因为默认所有约束的优先级都是 UILayoutPriorityRequired,所以我们只需要将 viewLeft 的优先级设置得比默认的低即可。

效果:

需要注意的一点是,约束的优先级必须在它添加到视图上之前设置,如果约束已经添加到视图上后去尝试改变它的优先级,将会得到一个异常。

提高效率

Auto Layout 虽然很好,但无论是直接使用 NSLayoutConstraint 还是使用 VFL 来编写布局的代码都比较麻烦。

好消息是有大量的开源库帮助我们提高编写布局代码的效率。比较流行的有:

我最初使用 UIView-AutoLayout,但因为它不支持 OSX,所以后来使用过一段时间的 Masonry,当 UIView-AutoLayout 的原作者发布 PureLayout 后,我就转向了 PureLayout 并使用至今。

在我看来,MasonryPureLayout 差别并不大,PureLayout 的语法更偏向Objective-C。

下面是一个 Instagram 页面截图,我们使用 PureLayout 来实现这个布局。

我把它分为头像、昵称、时间标识、时间、赞标识、赞的数量、赞按钮、评论按钮、更多按钮以及中间的图片视图。

声明以下属性:

@property (nonatomic, strong) UIImageView *avatarImageView;
@property (nonatomic, strong) UILabel     *nicknameLabel;
@property (nonatomic, strong) UIView      *timestampIndicator;
@property (nonatomic, strong) UILabel     *timestampLabel;
@property (nonatomic, strong) UIImageView *contentImageView;
@property (nonatomic, strong) UIView      *likeIndicator;
@property (nonatomic, strong) UILabel     *likesLabel;
@property (nonatomic, strong) UIButton    *likeButton;
@property (nonatomic, strong) UIButton    *commentButton;
@property (nonatomic, strong) UIButton    *moreButton;

布局代码如下:

// 头像左边距离父视图左边 10 点.
[self.avatarImageView autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:10.f];

// 头像顶边距离父视图顶部 10 点.
[self.avatarImageView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:10.f];

// 设置头像尺寸
[self.avatarImageView autoSetDimensionsToSize:kAvatarSize];

// 昵称的左边位于头像的右边 10 点的地方.
[self.nicknameLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:self.avatarImageView withOffset:10.f];

// 根据昵称的固有内容尺寸设置它的尺寸
[self.nicknameLabel autoSetDimensionsToSize:[self.nicknameLabel intrinsicContentSize]];

// 时间标识的右边位于时间视图左边 -10 点的地方, 从右往左、从下往上布局时数值都是负的。
[self.timestampIndicator autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:self.timestampLabel withOffset:-10.f];

// 根据时间标识的固有内容尺寸设置它的尺寸
[self.timestampIndicator autoSetDimensionsToSize:CGSizeMake(10.f, 10.f)];

// 时间视图的右边距离父视图的右边 10 点.
[self.timestampLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:10.f];

// 根据时间视图的固有内容尺寸设置它的尺寸
[self.timestampLabel autoSetDimensionsToSize:[self.timestampLabel intrinsicContentSize]];

// 头像、昵称、时间标识、时间视图水平对齐。(意思就是说只需要设置其中一个的垂直约束(y)即可)
[@[self.avatarImageView, self.nicknameLabel, self.timestampIndicator, self.timestampLabel] autoAlignViewsToAxis:ALAxisHorizontal];

// 内容图片视图顶部距离头像的底部 10 点.
[self.contentImageView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.avatarImageView withOffset:10.f];

// 内容图片视图左边紧贴父视图左边
[self.contentImageView autoPinEdgeToSuperviewEdge:ALEdgeLeading];

// 内容图片视图的宽度等于父视图的宽度
[self.contentImageView autoMatchDimension:ALDimensionWidth toDimension:ALDimensionWidth ofView:self];

// 内容图片视图的高度等于父视图的宽度
[self.contentImageView autoMatchDimension:ALDimensionHeight toDimension:ALDimensionWidth ofView:self];

// 赞标识与头像左对齐
[self.likeIndicator autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:self.avatarImageView];

// 赞标识的顶部距离内容图片视图底部 10 点.
[self.likeIndicator autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.contentImageView withOffset:10.f];

// 设置赞标识的尺寸
[self.likeIndicator autoSetDimensionsToSize:CGSizeMake(10.f, 10.f)];

// 赞数量视图与赞标识水平对齐
[self.likesLabel autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.likeIndicator];

// 赞数量视图的左边距离赞标识的右边 10 点.
[self.likesLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:self.likeIndicator withOffset:10.f];

// 以下请自行脑补...
[self.likesLabel autoSetDimensionsToSize:[self.likesLabel intrinsicContentSize]];

NSArray *buttons = @[self.likeButton, self.commentButton, self.moreButton];
[buttons autoMatchViewsDimension:ALDimensionHeight];
[buttons autoAlignViewsToEdge:ALEdgeBottom];
[self.likeButton autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:self.avatarImageView];
[self.likeButton autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10.f];
[self.likeButton autoSetDimensionsToSize:CGSizeMake(50.f, 25.f)];

[self.commentButton autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:self.likeButton withOffset:5.f];
[self.commentButton autoSetDimension:ALDimensionWidth toSize:65.f];

[self.moreButton autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:10.f];
[self.moreButton autoSetDimension:ALDimensionWidth toSize:40.f];

效果完成:

Demo 工程下载

作者

Vincent Sit

查看评论
下一篇

Auto Layout 中的对齐选项

上一篇

Xcode 5 自动读取注释增强 Quick Help