AutoLayout布局最佳实践

背景

iphone 发布历史

  1. 在iphone1-iphone3gs时代 window的size固定为(320,480),我们只需要简单计算一下相对位置就好了
  2. 在iphone4-iphone4s时代 苹果推出了retina屏,但是给了码农们非常大的福利:window的size不变
  3. 在iphone5-iphone5s时代,window的size变了(320,568),这时autoresizingMask派上了用场(为啥这时候不用Autolayout? 因为还要支持ios5呗)
  4. 在iphone6+时代 window的width也发生了变化(相对5和5s的屏幕比例没有变化),终于是时候抛弃autoresizingMask改用autolayout了(不用支持ios5了 相对于屏幕适配的多样性来说autoresizingMask也已经过时了)

布局方式变革

纯手写代码所经历的关于页面布局的三个时期

MagicNumber -> autoresizingMask -> autolayout
  1. 直接设置view的几何属性, 最为常见的就是直接设置frame
    • 通过直接对view几何属性值的设定到达期望的布局效果
    • 不能自动适配布局的变化,MagicNumber
  2. 在1的基础上为view设置AutoresizingMask
    • 用于描述一个view的superview的大小发生改变时,这个view的布局改如何调整
    • 最大的限制是autoresizingMask描述的变化特征只能限于view和其superview
  3. Auto Layout
    • 通过一系列的约束(constraints)来描述view间的布局关系, 系统会通过这些constraints来计算出view的几何属性
    • 使用繁琐和啰嗦 -> Masonry

Auto Layout 的基础概念

使用 Autolayout 的一般流程

  1. 添加约束之前必须将view添加到superview里
  2. 对于要使用Auto Layout的控件需要关闭Autoresizing
  3. 创建并添加约束
  4. 更新约束

约束(constraints)

在Apple的文档中有提到在autolayout系统中, view的布局是通过一系列的线性等式描述的. 而一个约束就是一个等式. 这其实是autolayout的实现原理。

可以把一个约束理解为描述一个view或是两个view之间的某个布局特性的关系()。

item1.attribute1 = multiplier ⨉ item2.attribute2 + constant
  • 除了constant之外, 其他的property都是readonly的
  • multiplier和constant都是CGFloat类型
  • relation描述了这个约束的等式关系, 除了相等之外还可以是小于等于和大于等于

优先级(priority)

NSLayoutConstraint中唯一一个没有出现在约束等式中的property就是优先级(priority), 它的类型是UILayoutPriority

enum {
   UILayoutPriorityRequired = 1000,
   UILayoutPriorityDefaultHigh = 750,
   UILayoutPriorityDefaultLow = 250,
   UILayoutPriorityFittingSizeLevel = 50,
};
typedef float UILayoutPriority;

NSLayoutConstraint的priority属性虽然没有被标识为readonly, 但是并不是能随意改变, 当一个约束被添加到view后, 以下两种情况会导致exception:

  • 降低一个原本优先级为1000(UILayoutPriorityRequired)的约束的优先级
  • 将一个原本优先级较1000低的约束的优先级设置为1000
    所以动态调整约束的优先级并不是很好的实践

视图(view)与约束

约束描述的是view的布局属性的关系, 但仅仅是把约束创建出来是不够的, 还要把约束添加到合适的view上这个约束才能生效

Auto Layout 要求约束(constraint)被添加到这个约束描述的两个view的公共superview上

111
222

Intrinsic Content Size

Content Hugging Priority
Content Compression Resistance Priority
  • intrinsicContentSize:字面意思就是固有的大小。就是说在没有受到约束影响时本来应该有的大小。
  • Content Hugging Priority:关于“是否将内容拉伸”的选项,当元素出现冲突时,会将Content Hugging Priority 高的一方维持原样,将低的一方拉伸。但此时仍会保持内容的正常显示。
  • Content Compression Resistance Priority:关于“是否将内容压缩”的选项,甚至会压缩到不能正常显示它的内容。当元素冲突时,会将 Content Compression Resistance Priority 低的一方压缩到合适的大小,高的一方尽量维持内容的显示。

Content Hugging Priority 以及 Content Compression Resistance Priority 都分别包含水平向(Horizontal),垂直向(Vertical)两个方向单独设置。

我们一般提及Compression-Resistance和Content-Hugging的时候说的就是这两组约束等式的优先级, Compression-Resistance的默认优先级是750, 而Content-Hugging的默认优先级是250.

使用AutoLayout时遇到问题

使用autolayout来布局可能会遇到以下几种错误导致布局问题

Ambiguous Layouts

提供的约束不充分, 如果用autolayout来实现布局的话, 每个view的横向和纵向都需要两个约束(intrinsicContentSize可以认为是约束), 要是我们提供的约束不充分的话, 系统在根据约束布局时view的某个几何特性得不到确定的解, 也就是所说的二义性, 这是系统会使用一个不确定的值来填充. UIView的- (BOOL)hasAmbiguousLayout方法可以在运行时来验证某个view是否存在Ambiguous Layouts

Unsatisfiable Layouts

提供的约束不能同时被满足, 比如一个约束说view的宽是10, 另一个约束说宽是8, 两个的优先级又相同的情况下, 就会出现这种情况. 系统会在console里面打印说这两个约束出现了冲突(conflict).

由于系统不能同时满足这两个约束, 所以系统会选取一条约束来break, 就是说不满足这一条了, 这样来给出一个结果. 但至于选取哪一条是不确定的.

Unsatisfiable Layouts是比较严重的问题, 不仅我们得不到想要的布局效果,在老的iOS版本还可能会引起APP的crash. 所以遇到这个错误一定要分析解决掉

布局流程

完整的布局流程

从约束被更新到view被显示到屏幕上经历了上图中从左到右3个周期
1. 自下而上(先子view再父view)的约束更新周期, 这个周期相关的方法标注为红色
2. 自上而下(先父view再子view)的布局周期, 这个周期相关的方法标注为黄色
3. 自上而下的绘制周期, 这个周期相关的方法标注为蓝色

每个周期可以通过调用对应的方法来触发(Trigger), 系统会在每个周期调用相应的方法, 我们可以重载(Override)这些方法来实现自定义的布局逻辑, 后面会提到使用这些方法的注意事项
跟老的方式一样, 布局流程是一个和系统runloop配合循环往复的过程

Constraints Change

系统会在每个runloop都去检查布局系统中的约束表达式是否发生了变化, Apple提到以下几点会引起布局约束表达式变化:

  • 某个约束被Activating或是被Deactivating(iOS8及以后)
  • 改变某个约束的constant或priority
  • 添加或是移除view

如果约束表达式发生了变化, autolayout系统会根据新的表达式计算出view新的几何属性(这时并没有根据新的值来布局view), 得到新的几何属性的view将调用其superview的setNeedsLayout方法(这样在接下来的布局周期时系统根据新的几何属性来布局这个view)

Deferred Layout Pass

这个阶段包含了下面两个周期

  1. Update constraints
    之前提到过, 这个周期通过调用-setNeedsUpdateConstraints来触发, 系统会调用-updateConstraints这个方法, 我们可以重载这个方法来做一些更新约束相关的事情, 但在重载时要注意以下几点

    • 不要在这个方法里面做会让约束失效的事, 比如移除约束或是移除view
    • 不要在这个方法里面调用跟Layout和Display周期相关的方法
    • 一定要在方法的最后调用 [super updateConstraints]
  2. Layout
    这个周期通过调用-setNeedsLayout来触发, 系统会调用-layoutSubviews这个方法, 我们可以重载这个方法来直接设置子view几何属性, 建议只在用来完成不能通过约束来实现的布局效果时重载, 注意以下几点:

    • 不要忘记调用[super layoutSubviews]
    • 不要改变任何不在这个view子树里面的view的几何属性
    • 不要调用-setNeedsUpdateConstraints
    • 不要在这里修改布局约束

Masonry 使用

Masonry是一个轻量级的布局框架 拥有自己的描述语法 采用更优雅的链式语法封装自动布局 简洁明了 并具有高可读性 而且同时支持 iOS 和 Max OS X

Masonry支持哪一些属性

@property (nonatomic, strong, readonly) MASConstraint *left;
@property (nonatomic, strong, readonly) MASConstraint *top;
@property (nonatomic, strong, readonly) MASConstraint *right;
@property (nonatomic, strong, readonly) MASConstraint *bottom;
@property (nonatomic, strong, readonly) MASConstraint *leading;
@property (nonatomic, strong, readonly) MASConstraint *trailing;
@property (nonatomic, strong, readonly) MASConstraint *width;
@property (nonatomic, strong, readonly) MASConstraint *height;
@property (nonatomic, strong, readonly) MASConstraint *centerX;
@property (nonatomic, strong, readonly) MASConstraint *centerY;
@property (nonatomic, strong, readonly) MASConstraint *baseline;

[基础] 居中显示一个view

//从此以后基本可以抛弃CGRectMake了
UIView *sv = [UIView new];

//在做autoLayout之前 一定要先将view添加到superview上 否则会报错
[self.view addSubview:sv];

//mas_makeConstraints就是Masonry的autolayout添加函数 将所需的约束添加到block中行了
[sv mas_makeConstraints:^(MASConstraintMaker *make) {

    //将sv居中
    make.center.equalTo(self.view);

    //将size设置成(300,300)
    make.size.mas_equalTo(CGSizeMake(300, 300));
}];

这里有两个问题要分解一下

  • 首先在Masonry中能够添加autolayout约束有三个函数
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;

/*
    mas_makeConstraints 只负责新增约束 Autolayout不能同时存在两条针对于同一对象的约束 否则会报错,不太适合写在-updateConstraints 或-updateViewConstraints里

    mas_updateConstraints 针对上面的情况 会更新在block中出现的约束 不会导致出现两个相同约束的情况,比较适合写在-updateConstraints 或-updateViewConstraints里。

    mas_remakeConstraints 则会清除之前的所有约束 仅保留最新的约束。因为约束的添加和删除都是相对耗时的操作,尤其是在布局层级深又复杂的情况下,因此使用时还是应该慎重,某些场景会影响FPS

    三种函数善加利用 就可以应对各种情况了
*/
  • 其次 equalTo 和 mas_equalTo的区别在哪里呢? 其实 mas_equalTo是一个MACRO
#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...)    greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...)       lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))

#define mas_offset(...)                  valueOffset(MASBoxValue((__VA_ARGS__)))

可以看到 mas_equalTo只是对其参数进行了一个BOX操作(装箱) MASBoxValue的定义具体可以看看源代码 太长就不贴出来了

所支持的类型 除了NSNumber支持的那些数值类型之外 就只支持 CGPointCGSizeUIEdgeInsets

[初级] 让一个view略小于其superView(边距为10)

UIView *sv1 = [UIView new];
[sv1 showPlaceHolder];
sv1.backgroundColor = [UIColor redColor];
[sv addSubview:sv1];
[sv1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(sv).with.insets(UIEdgeInsetsMake(10, 10, 10, 10));

    /* 等价于
    make.top.equalTo(sv).with.offset(10);
    make.left.equalTo(sv).with.offset(10);
    make.bottom.equalTo(sv).with.offset(-10);
    make.right.equalTo(sv).with.offset(-10);
    */

    /* 也等价于
    make.top.left.bottom.and.right.equalTo(sv).with.insets(UIEdgeInsetsMake(10, 10, 10, 10));
    */
}];

这里有意思的地方是 andwith 其实这两个函数什么事情都没做。

但是用在这种链式语法中,就非常的巧妙和易懂。

- (MASConstraint *)with {
    return self;
}

- (MASConstraint *)and {
    return self;
}

[中级] 在UIScrollView顺序排列一些view并自动计算contentSize

UIScrollView *scrollView = [UIScrollView new];
scrollView.backgroundColor = [UIColor whiteColor];
[sv addSubview:scrollView];
[scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(sv).with.insets(UIEdgeInsetsMake(5,5,5,5));
}];

UIView *container = [UIView new];
[scrollView addSubview:container];
[container mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(scrollView);
    make.width.equalTo(scrollView);
}];

int count = 10;

UIView *lastView = nil;

for ( int i = 1 ; i <= count ; ++i )
{
    UIView *subv = [UIView new];
    [container addSubview:subv];
    subv.backgroundColor = [UIColor colorWithHue:( arc4random() % 256 / 256.0 )
                                      saturation:( arc4random() % 128 / 256.0 ) + 0.5
                                      brightness:( arc4random() % 128 / 256.0 ) + 0.5
                                           alpha:1];

    [subv mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.and.right.equalTo(container);
        make.height.mas_equalTo(@(20*i));

        if ( lastView ) {
            make.top.mas_equalTo(lastView.mas_bottom);
        } else {
            make.top.mas_equalTo(container.mas_top);
        }
    }];

    lastView = subv;
}


[container mas_makeConstraints:^(MASConstraintMaker *make) {
    make.bottom.equalTo(lastView.mas_bottom);
}];

代码规范

对于使用autolayout布局的view, 请不要再设置其几何属性

AutoLayout 只有约束的概念,忘记 frame 的概念。

保证对 view 进行自动布局之前已经将该 View及相关View添加到super view中

否则会 crash

在哪里建立约束

  • VC的 viewDidLoad, 在这里可以建立VC根view及其子view间的约束
  • 自定义view的init方法, 可以建立view和其子view的约束

- updateConstraints- updateViewConstraints 里更新约束, 不要建立新的约束

  • 一定要在方法的最后调用 [super updateConstraints]
  • 不要使用- mas_makeConstraints进行布局设置, 可能多次调用
  • 不要在这个方法里面做会让约束失效的事, 比如移除约束或是移除view
  • 不要在这个方法里面调用跟Layout和Display周期相关的方法

自定义的 View 重写+ requiresConstraintBasedLayout 并返回 YES

可以保证 Auto Layout 设置生效, 否则在某些情况下 Auto Layout 可能不生效

除非必要,否则尽量不要调用- updateConstraintsIfNeeded

影响性能,尽量调用- setNeedsUpdateConstraints

消除约束的警告

Unsatisfiable Layouts 在低版本设备上会导致 Crash

有需要可以设置mas_key方便调试

不要动态的调整约束的优先级

可能引起异常

尽量保证单向布局特性依赖关系

  • 子view依赖父view, 父view绝不依赖子view
  • 保证代码的可读性

谨慎(尽量不要)对使用autolayout布局的view调用从视图层级上移除的方法(removeFromSuperview, removeAllSubview)

谨慎(尽量不要)移除constraints(removeConstraint, mas_remakeConstraints)

Masonry的 make/update/remake 用的 block 不写 weak self

这里用到的 Block 不会被持有,所以不会引起循环引用,所以不需要写 weak self ,为了代码整洁性,要求这里不写代码

Masonry 的 with 和 and 没有实际功能,为了语义的完整性建议写上,但是不强制要求

参考

推荐阅读更多精彩内容

  • 目录 0、前言 一、Auto Layout前世今生 二、Auto Layout基础知识 1.Auto Layout...
    浮游lb阅读 17,541评论 3 79
  • 项目里的布局一直都是纯代码流,顺带着Autolayout也一直没有使用,直到遇到了masonry,让我看到了希望,...
    小笨狼阅读 8,267评论 25 127
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 7,119评论 4 39
  • 非游戏类,初级面试常见问题1.你昨天/这周学习了什么?坦白点说,学习笔试面试后发现自己知识点不足的地方2.你为什么...
    cj2527阅读 480评论 0 0
  • 没有人可以说心里话是一种什么样的体验,恐怕每个人都有这个时候。 不是只要有人听就行的,这个人一定要是一个对的人,一...
    多角章鱼阅读 71评论 0 0