Auto Layout是iOS 6以后Apple提供的布局界面的新方法,用来简化界面布局的代码。
本质上,界面布局都是确定下每一个View的位置和大小。传统的手写代码布局有两种方式,一种是根据不同的屏幕大小,加载不同常数。这种方式只适用于静态布局,View的大小一开始就确定了,或者说View的大小和变化一开始就能预料到。另外一种就是考虑View的位置关系,只用极少的几个常数确定一个或者几个View的位置,其他的View都是相对于这几个View计算。第二种的布局方式其实已经是Auto Layout的思想了,只不过Apple做的更好。
View布局的一个基本公式是:
view1.attr1 = multi * view2.attr2 + constant
翻译出来就是:第一个view的某一个属性的值是第二个view的某一个属性的值乘以一个系数再加上一个常数。其中等于可以改成大于等于或者小于等于。
可以使用的属性有:
Top, Bottom, Left or Leading, Right or Trailing, Width, Height,
CenterX, CenterY, Baseline,LastBaseLine, FirstBaseLine, LeftMargin,
RightMargin, TopMargin, BottomMargin, LeadingMargin, TrailingMargin,
CenterXWithMargins, CentYWithMargins
显然,并不是所有的属性之间都能建立起上述关系的。比如width和left显然就不能,一般都很好理解,不会造成困惑。当然,如果你用错了,会有运行时错误发生。
基本用法
如果使用Auto Layout的API来添加约束,一般会是这样:
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:v1 attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual toItem:self.view
attribute:NSLayoutAttributeTop multiplier:1.0f constant:30]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:v1 attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual toItem:self.view
attribute:NSLayoutAttributeLeading multiplier:1.0f constant:30]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:v1 attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual toItem:nil
attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:100]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:v1 attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual toItem:nil
attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:100]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:v2 attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual toItem:v1
attribute:NSLayoutAttributeBottom multiplier:1.0f constant:8]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:v2 attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual toItem:self.view
attribute:NSLayoutAttributeLeading multiplier:1.0f constant:30]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:v2 attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual toItem:v1
attribute:NSLayoutAttributeWidth multiplier:1.0f constant:0]];
[self.view addConstraint:[NSLayoutConstraint
constraintWithItem:v2 attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual toItem:v1
attribute:NSLayoutAttributeHeight multiplier:1.0f constant:0]];
Apple提供了一种简写的方式,叫做VFL(Visual Format Language)可以将上边的代码简化一点:
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"V:|-30-[v1(100)]"
options:0 metrics:nil views:NSDictionaryOfVariableBindings(v1)]];
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"H:|-30-[v1(100)]"
options:0 metrics:nil views:NSDictionaryOfVariableBindings(v1)]];
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"V:[v1]-[v2(v1)]"
options:0 metrics:nil views:NSDictionaryOfVariableBindings(v1, v2)]];
[self.view addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:@"H:|-30-[v2(v1)]"
options:0 metrics:nil views:NSDictionaryOfVariableBindings(v1, v2)]];
然而学习VFL并不好读,并且需要学习成本。目前业界比较通用的方案是使用开源的Masonry。
Auto Layout要求约束必须恰好能固定各个View的大小和位置,如果约束不足,则View显示不出来,如果约束过多,可能会引起冲突。(可以将约束不足的看做是view位置的欠定方程组,约束过多的看做是超定方程组)。比如上边的例子,确定两个view需要8个参数,那么就需要解8个约束的方程组。如果超过8个约束,除非有多个约束是等价的,否则会引起约束冲突,造成显示不正确。
约束的优先级
约束发生冲突的时候,首先的解决方式是查看约束的优先级,Auto Layout会打破低优先级的约束以满足高优先级的约束。约束的优先级是一个1-1000的整数,数字越大优先级越高。优先级为1000的约束叫做required约束,是不能被打破的,如果两个优先级为1000的约束发生冲突,则Auto Layout无法解决冲突,有时候会引起程序的Crash。通常的做法是将要求比较高的约束优先级设定为999,这样至少可以保护程序不Crash。
对于约束不足的情况,Auto Layout无法解决。
一旦使用了Auto Layout,设置View的Frame就不起作用了。
Auto Layout 不仅仅能让我们通过添加约束来布局界面。UIView和UIViewController上添加了许多属性和方法,来配合Auto Layout完成更高级的功能。
Intrinsic Content Size
intrinsicContentSize
是UIView
上的一个属性。这个属性表示这个View的固有大小。什么是固有大小呢?比如说,一个UIImageView
加载了一幅图片,这个图片的大小就是这个UIImageView
的固有大小,再比如说,一个UILabel
,固定了字体字号,text之后text所占据的空间就是固有大小。一个自定义的View,默认是没有固有大小的(返回(-1,-1))。
从固有大小的定义可以看出,固有大小是根据内容动态算出来的。那么如果让自定义的View有固有大小,可以重写intrinsicContentSize
的getter方法,使用自己的规则返回这个View的固有大小。同样如果重写UIImageView
和UILabel
的intrinsicContentSize
的getter,也能改变系统的固有大小计算方式。
那么固有大小有什么用呢?
在Auto Layout中,如果一个View有intrinsicContentSize
,在计算约束的时候会自动根据intrinsicContentSize
添加宽和高的约束。也就是说,对于UILabel
和UIImageView
这类的View,只需要添加两个位置约束就能确定他们的布局。
事实上,Auto Layout并不仅仅是将intrinsicContentSize
转换成确定的宽和高的约束,而是转化为两对优先级不同的约束,成为resistance约束和hugging约束。
resistance 约束优先级默认为750:
view.width >= intrinsicContentSize.width
view.height >= intrinsicContentSize.height
hugging约束 优先级默认为250:
view.width <= intrinsicContentSize.width
view.height <= intrinsicContentSize.height
将约束分为resistance和hugging可以将拉伸和压缩操作分开,如果我们在外部设置了优先级500的Size约束,就可以在空间足够的时候拉伸,在空间不够的时候却不压缩以保证内容显示的完整性。如果只是用一个约束,将无法做到这一点。
在有多个有intrinsicContentSize
的View共同布局的时候,需要手动调整约束的优先级,否则大家的优先级都一样,不能确定哪个该拉伸。
Top Layout Guide & Bottom Layout Guide
iOS开发中不可避免的要遇到涉及status bar,top bar,或者bottom bar的布局问题。比如会有些交互是隐藏或者显示TabViewController
底部的bar,或者NavigationViewController
顶部的bar。
iOS 7为UIViewController
添加了两个属性:
//为了排版,这里简化了源代码。
@interface UIViewController (UILayoutSupport)
@property (nonatomic, readonly) id<UILayoutSupport> topLayoutGuide;
@property (nonatomic, readonly) id<UILayoutSupport> bottomLayoutGuide;
@end
这两个属性提供了当前UIViewController
的root view的可视范围。隐藏或者显示top bar或者bottom bar会影响两个属性。比如,不隐藏status bar,topLayoutGuide.length
就是20.0,隐藏了status bar, topLayoutGuide.length
就是0.0f。可以在ViewController中使用下边代码实验:
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
NSLog(@"top guide length: %f", self.topLayoutGuide.length);
}
- (BOOL)prefersStatusBarHidden {
return YES;
}
在iOS 9以前UILayoutSupport这个协议只有一个length属性。并且,这个属性不是给Auto Layout用的。
As a courtesy when not using auto layout, this value is safe to refer to in -viewDidLayoutSubviews, or in -layoutSubviews after calling super。
UILayoutSupport
的length属性在不使用Auto Layout的情况下,在viewDidLayoutSubviews
或者调用super的layoutSubviews
之后才起作用。使用传统的代码布局方式要在这个阶段根据获取的topGuide或者bottomGuide来调整Subview的Frame。
iOS 9以后,使用Auto Layout可以这样处理:
NSLayoutConstraint *top =
[v1.topAnchor constraintEqualToAnchor:self.topLayoutGuide.bottomAnchor];
NSLayoutConstraint *bottom =
[v1.bottomAnchor constraintEqualToAnchor:self.bottomLayoutGuide.topAnchor];
NSLayoutConstraint *leading =
[v1.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor];
NSLayoutConstraint *trailing =
[v1.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor];
NSLayoutAnchor是iOS 9引入的特性,是为了简化和封装NSLayoutConstraint的创建的。每个UIView上也增添了一些Layout Anchor属性:
@interface UIView (UIViewLayoutConstraintCreation)
/* Constraint creation conveniences. See NSLayoutAnchor.h for details.
*/
@property(readonly) NSLayoutXAxisAnchor *leadingAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly) NSLayoutXAxisAnchor *trailingAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly) NSLayoutXAxisAnchor *leftAnchor NS_AVAILABLE_IOS(9_0);
@property(readonly) NSLayoutXAxisAnchor *rightAnchor NS_AVAILABLE_IOS(9_0);
....
@end
这些Anchor封装了生成NSLayoutConstraint
的方法constraintEqualToAnchor(还有其他的生成方法,这里没有列举),所以在iOS 9以后使用Anchor创建NSLayoutConstraint
也是一种简化方式。
但是,对于UIViewController
上的topLayoutGuide
和bottomLayoutGuide
,它们本身就是一个UILayoutSupport
接口对象,并不是一个UIView,因此只能用Anchor的方式来创建约束,而不能通过之前的constraintWithItem:
来创建(注意:UILayoutSupport
的length属性是只有在布局完成后才生效,而添加约束一般是在layout之前,因此一般也不能在通过它来获取topLayoutGuide
或者bottomLayoutGuide
的高度,所以很遗憾的UIViewController
的topLayoutGuide
和bottomLayoutGuide
的Auto Layout特性只有在iOS 9以后才能用)。
Margin
什么是Margin?
Margin是一个View的边缘和它的SubView边缘应该保持的距离。就是俗称的“留个边儿”。默认的,Margin大小是8个point。
Margin是iOS 8.0引入的概念,所以在UIView上添加了相应的属性和方法:
@property (nonatomic) UIEdgeInsets layoutMargins NS_AVAILABLE_IOS(8_0);
@property (nonatomic) BOOL preservesSuperviewLayoutMargins NS_AVAILABLE_IOS(8_0);
- (void)layoutMarginsDidChange NS_AVAILABLE_IOS(8_0);
layoutMargins
标识了这个View的上下左右Margin有多大。
preservesSuperviewLayoutMargins
是控制和父View的Margin的关系的。
layoutMarginsDidChange
是Margin变化后的回调。
The system sets and manages the margins of a view controller’s root view. The top and bottom margins are set to zero points, making it easy to extend content under the bars (if any). The side margins vary depending on how and where the controller is presented, but can be either 16 or 20 points. You cannot change these margins.
官方文档上的这段话是说UIViewController的rootView已经被做了特殊处理,Margin并不是默认的8个point。
Margin和Auto Layout
在Subview和SuperView之间添加约束的时候,如果想要添加约束到SuperView的Margin,使用UIView上的LayoutMarginGuide属性,当然这个属性也是iOS 9.0之后才支持。
上文中的leading和trailing约束可以这样生成:
NSLayoutConstraint *leading =
[v1.leadingAnchor constraintEqualToAnchor:self.view.layoutMarginsGuide.leadingAnchor];
NSLayoutConstraint *trailing =
[v1.trailingAnchor constraintEqualToAnchor:self.view.layoutMarginsGuide.trailingAnchor];
将leading和trailing的约束改成相对于UIViewController
的View的Margin之后,v1的左边和右边将和SuperView保持16 points的距离。原因在上边说了,UIViewController
的rootView的Margin默认不是8 points。
事实上,我个人的经验来说,设计师们关心的并不是subview到superview的Margin距离是多少,她们只关心绝对距离有多少,因此在我的实际开发中,绑定约束到Margin的情况并不多见。如果对于不同大小的屏幕,边距要求是不同的,那么将约束绑定到Margin,然后通过修改Margin来控制边距也是一种思路。
UITableViewCell的高度
Auto Layout有一个很重要的应用就是计算UITableViewCell
的高度。
一般变高的UITableViewCell的解决方案都是使用一个原型Cell,将数据填充到这个原型Cell中计算出高度。iOS8之前和iOS8之后的Cell高度计算次数和时机是不同的。有关UITableViewCell高度的细节参见相关文章,这里推荐一篇: 优化UITableViewCell高度计算的那些事。
使用Auto Layout可以简化计算,将Cell的SubView设置好约束,然后填充好内容,使用UIView上的systemLayoutSizeFittingSize
:方法,能根据View的SubViews的约束来计算Superview的大小。
参考文献
- Apple 官方文档
- 《Programming iOS 10》Mutt Neuburg
- 优化UITableViewCell高度计算的那些事