Auto Layout

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

intrinsicContentSizeUIView上的一个属性。这个属性表示这个View的固有大小。什么是固有大小呢?比如说,一个UIImageView加载了一幅图片,这个图片的大小就是这个UIImageView的固有大小,再比如说,一个UILabel,固定了字体字号,text之后text所占据的空间就是固有大小。一个自定义的View,默认是没有固有大小的(返回(-1,-1))。

从固有大小的定义可以看出,固有大小是根据内容动态算出来的。那么如果让自定义的View有固有大小,可以重写intrinsicContentSize的getter方法,使用自己的规则返回这个View的固有大小。同样如果重写UIImageViewUILabelintrinsicContentSize的getter,也能改变系统的固有大小计算方式。

那么固有大小有什么用呢?
在Auto Layout中,如果一个View有intrinsicContentSize,在计算约束的时候会自动根据intrinsicContentSize添加宽和高的约束。也就是说,对于UILabelUIImageView这类的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上的topLayoutGuidebottomLayoutGuide,它们本身就是一个UILayoutSupport接口对象,并不是一个UIView,因此只能用Anchor的方式来创建约束,而不能通过之前的constraintWithItem:来创建(注意:UILayoutSupport的length属性是只有在布局完成后才生效,而添加约束一般是在layout之前,因此一般也不能在通过它来获取topLayoutGuide或者bottomLayoutGuide的高度,所以很遗憾的UIViewControllertopLayoutGuidebottomLayoutGuide的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的大小。

参考文献

  1. Apple 官方文档
  2. 《Programming iOS 10》Mutt Neuburg
  3. 优化UITableViewCell高度计算的那些事
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值