本文是博主 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)
元素之间可以有关系。例如在键盘上
不理解?试着把键盘想象成
约束(Constraint)
元素之间关系的限制。约束是 Auto Layout 系统中最重要的概念。我们上面提到的
描述(Description)
定义约束来限制元素之间的关系。描述定义了元素之间的关系及约束。
继续用键盘举例,Q
现在
忘掉传统的 Springs & Struts 布局方式
事实上如果你用传统的设置 frame 的布局方式的思维来理解上面的
因为在 Auto Layout 中,当你描述完之后, Auto Layout 会自动帮你计算出 frame。换句话说,你的描述告诉了 Auto Layout 如何帮你计算出 frame。所以,你也可以理解为你间接的设置了 frame。为什么要这么做呢?为什么不直接设置 frame?这是因为使用 Auto Layout 有很多好处:
- 多数情况下旋转屏幕不用再做额外的处理
- 更容易适配不同尺寸的屏幕
- 上手后布局非常简单容易,布局逻辑更清晰
Auto Layout 和传统布局很大的不同之处在于它是一种相对的布局方式。怎么理解这句话?上面提到
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 时,首先需要将视图的translatesAutoresizingMa
Auto Layout 基础知识
无论是在 Xib 中还是代码中使用 Auto Layout,你都需要了解 Auto Layout 的一些必要知识。这些你现在不理解没有关系,后面我们会详细讲述。
约束 (Constraint)
Auto Layout 中约束对应的类为
NSLayoutConstraint
+ (id)constraintWithItem:(id)view1
不要被这个方法的参数吓到,实际上它只做一件事,就是让
这里的
精简后就是下面这个公式:
view1.attribute1 = view2.attribute2 × multiplier + constant
还有一个参数是
需要注意的是,≤
例子:
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 setTranslatesAutoresizin
// `view` 的左边距离 `self.view` 的左边 50 点.
NSLayoutConstraint *viewLeft = [NSLayoutConstraint constraintWithItem:view
// `view` 的顶部距离 `self.view` 的顶部 100 点.
NSLayoutConstraint *viewTop = [NSLayoutConstraint constraintWithItem:view
// `view` 的宽度 是 60 点.
NSLayoutConstraint *viewWidth = [NSLayoutConstraint constraintWithItem:view
// `view` 的高度是 60 点.
NSLayoutConstraint *viewHeight = [NSLayoutConstraint constraintWithItem:view
// 把约束添加到父视图上.
[self.view addConstraints:@[viewLeft, viewTop, viewWidth, viewHeight]];
实现一个如此简单的布局竟然要写这么多的代码,这显然难于推广使用。于是 UIKit 团队发明了另外一种更简便的表达方式进行布局,这个我们后面再讲,现在先看看这段代码。
首先我把
然后在设置
然后在设置
因为 Auto Layout 是相对布局,所以通常你不应该直接设置宽度和高度这种固定不变的值,除非你很确定视图的宽度或高度需要保持不变。
如果一定要设置高度或宽度,特别是宽度,在没有显式地设置内容压缩优先级(Content Hugging Priority,后面会讲到)和内容抗压缩优先级(Content Compression Resistance Priority,后面会讲到)的情况下,尽量不要使用
- 根据内容决定宽度的视图,当内容改变时,外观尺寸无法做出正确的改变
- 在本地化时过长的文字无法显示,造成文字切断,或文字过短,宽度显得过宽,影响美观
- 添加了多余的约束时,约束之间冲突,无法显示正确的布局
所带来的问题不仅仅局限与这几条,这里只是简单列出几条。
如何正确的设置宽度或高度?给出一些 Tips:
- 如果宽度和高度布局可以改变,使用固有内容尺寸(Intrinsic Content Size,后面会讲到)设置约束(即size to fit size)。
- 如果宽度和高度布局不可以改变,改变约束的关系为
≥。 - 调整压缩优先级和内容抗压缩优先级
最后我把所有约束都添加到了
- 两个同层级间视图的约束,添加到它们共同的父视图上
- 两个不同层级间视图的约束,添加到它们最近的共同的父视图上
- 两个有层级关系的视图的约束,添加到层次较高的视图上(父视图)上
因为我们属于最后一种情况,所以子视图
接下来是第二个方法
+ (NSArray *)constraintsWithVisualFor
这个方法是我们实际编程中最常用的方法。它会根据我们指定的参数返回一组约束。 这个方法很重要,所以我会详细解释每个参数的用途。
format
这个参数存放的是布局逻辑,布局逻辑是使用
上一个布局使用
....
[view setTranslatesAutoresizin
NSDictionary *views = NSDictionaryOfVariableBi
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
哗,代码量减少了很多。首先我们使用
{@"self.view": self.view, @"view", view}
VFL
H:|-50-[view(>=150)]
V:|-100-[view(>=150)]
第一句是在水平方向布局,表示
第二句是在垂直方向上布局,表示
分解说明如下:
H
|
-
[]
VFL
- 布局语句中不能包含空格
- 和关系一样,没有
>、<</span> 这种约束
然后下面是一些例子,增加你对
例一:
我们在
代码如下:
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 setTranslatesAutoresizin
[view2 setTranslatesAutoresizin
NSDictionary *views = NSDictionaryOfVariableBi
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
我们讲讲最后的两条新的
H:[view]-[view2(>=50)]
从开始的
V:|-100-[view2(>=50)]
从开始的
实际上我们的代码还可以简化:
......
NSDictionary *views = NSDictionaryOfVariableBi
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
因为两个视图水平方向上是并排(从左到右)的,所以我们可以将水平方向布局的代码合并到一起。而垂直方向我们并非并排的,所以垂直方向的布局代码我们不能合并。这里所讲的并排的意思是后一个在前一个的后面,水平方向上明显是这样,但垂直方向上两个视图的
例二:我们继续添加一个视图
代码如下:
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 setTranslatesAutoresizin
[view2 setTranslatesAutoresizin
[view3 setTranslatesAutoresizin
NSDictionary *views = NSDictionaryOfVariableBi
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
你可能注意到我把每个间距都使用小括号阔了起来,这是可选的,你完全可以直接写间距,这么写只是告诉你还有这种语法。实际上没什么必要这么写,因为
最后两行是
H:[view]-[view3(>=50)]
水平方向布局,view3
V:|-(100)-[view2(>=50)][view3(>=100)]
垂直方向布局,view2
options
这个参数的值是位掩码,使用频率并不高,但非常有用。它可以操作在
......
NSDictionary *views = NSDictionaryOfVariableBi
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
它的默认值是
这个值符合我们常用的选项。NSLayoutFormatDirectionL
因为是位掩码,所以我们可以使用
......
NSDictionary *views = NSDictionaryOfVariableBi
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
指定两个视图的顶部和底部约束相同,然后只设置其中一个视图的相关约束即可。
灵活使用此参数可以节省不少时间,但这个参数内容太多,如果你有兴趣了解,可以看看我的另一篇博文:《Auto Layout 中的排列选项》
metrics
这是一个字典,字典的键必须是出现在
UIView *view = [UIView new];
[view setBackgroundColor:[UIColor redColor]];
[self.view addSubview:view];
[view setTranslatesAutoresizin
CGRect viewFrame = CGRectMake(50.f, 100.f, 150.f, 150.f);
NSDictionary *views = NSDictionaryOfVariableBi
NSDictionary *metrics = @{@"left": @(CGRectGetMinX(viewFrame)),
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
聪明的你看了这段代码后肯定已经明白这个参数的用途了,虽然使用频率不高,但依然很有用,特别是要动态计算约束值的时候非常有用。
实际上这个参数也可以使用
......
[view setTranslatesAutoresizin
NSNumber *left = @50.f;
NSNumber *top = @100.f;
NSNumber *width = @150.f;
NSNumber *height = @150.f;
NSDictionary *views = NSDictionaryOfVariableBi
NSDictionary *metrics = NSDictionaryOfVariableBi
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFor
views
又是一个字典,包含了
讲了这么多,可能你也发现了,只要学会了
优先级 (Priority level)
约束条件有优先级,高优先级约束会比低优先级约束优先得到满足,系统内置了 4 个优先级:
enum {
};
typedef float UILayoutPriority;
- UILayoutPriorityRequired
这是默认值,这意味着这个约束条件必须被精确地满足。 - UILayoutPriorityDefaultH
igh - UILayoutPriorityDefaultL
ow - UILayoutPriorityFittingS
izeLevel 这是内置的最低优先级。
相信你已经看到每个等级的数值了,优先级的取值在
每个约束的默认优先级就是
举个例子说明优先级设置不当的情况,给我们首次使用 Auto Layout 时的例子再添加一个约束:
......
// `view` 的高度是 60 点.
NSLayoutConstraint *viewHeight = [NSLayoutConstraint constraintWithItem:view
// `view` 紧贴着 `self.view` 的左边.
NSLayoutConstraint *marginLeft = [NSLayoutConstraint constraintWithItem:view
// 把约束添加到父视图上.
[self.view addConstraints:@[viewLeft, viewTop, viewWidth, viewHeight, marginLeft]];
运行看看效果,程序 Crash 了!控制台 Log 中有这么一段信息:
"",
""
可以看到第一条是
第二条是新添加的
很明显这两个约束是冲突的,当系统尝试根据优先级进行布局时,发现它们的优先级也相同,无法满足两个冲突的约束,所以抛出了异常。
我们只需要给两个约束设置不同的优先级即可解决。添加下面一行代码:
[viewLeft setPriority:UILayoutPriorityDefaultH
因为默认所有约束的优先级都是
效果:
需要注意的一点是,约束的优先级必须在它添加到视图上之前设置,如果约束已经添加到视图上后去尝试改变它的优先级,将会得到一个异常。
提高效率
Auto Layout 虽然很好,但无论是直接使用
好消息是有大量的开源库帮助我们提高编写布局代码的效率。比较流行的有:
我最初使用
在我看来,Masonry
下面是一个 Instagram 页面截图,我们使用
我把它分为头像、昵称、时间标识、时间、赞标识、赞的数量、赞按钮、评论按钮、更多按钮以及中间的图片视图。
声明以下属性:
@property (nonatomic, strong) UIImageView *avatarImageView;
@property (nonatomic, strong) UILabel
@property (nonatomic, strong) UIView
@property (nonatomic, strong) UILabel
@property (nonatomic, strong) UIImageView *contentImageView;
@property (nonatomic, strong) UIView
@property (nonatomic, strong) UILabel
@property (nonatomic, strong) UIButton
@property (nonatomic, strong) UIButton
@property (nonatomic, strong) UIButton
布局代码如下:
// 头像左边距离父视图左边 10 点.
[self.avatarImageView autoPinEdgeToSuperviewEd
// 头像顶边距离父视图顶部 10 点.
[self.avatarImageView autoPinEdgeToSuperviewEd
// 设置头像尺寸
[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:ALEdgeLeadingofView:self.timestampLabel withOffset:-10.f];
// 根据时间标识的固有内容尺寸设置它的尺寸
[self.timestampIndicator autoSetDimensionsToSize:CGSizeMake(10.f, 10.f)];
// 时间视图的右边距离父视图的右边 10 点.
[self.timestampLabel autoPinEdgeToSuperviewEd
// 根据时间视图的固有内容尺寸设置它的尺寸
[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 autoPinEdgeToSuperviewEd
// 内容图片视图的宽度等于父视图的宽度
[self.contentImageView autoMatchDimension:ALDimensionWidth toDimension:ALDimensionWidthofView:self];
// 内容图片视图的高度等于父视图的宽度
[self.contentImageView autoMatchDimension:ALDimensionHeight toDimension:ALDimensionWidthofView: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 autoPinEdgeToSuperviewEd
[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 autoPinEdgeToSuperviewEd
[self.moreButton autoSetDimension:ALDimensionWidth toSize:40.f];
应用内测平台 Pre.im