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上

AutoLayout布局最佳实践_第1张图片
111
AutoLayout布局最佳实践_第2张图片
222

Intrinsic Content Size

AutoLayout布局最佳实践_第3张图片
Content Hugging Priority
AutoLayout布局最佳实践_第4张图片
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. 所以遇到这个错误一定要分析解决掉

布局流程

完整的布局流程

AutoLayout布局最佳实践_第5张图片

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

AutoLayout布局最佳实践_第6张图片

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

AutoLayout布局最佳实践_第7张图片

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 没有实际功能,为了语义的完整性建议写上,但是不强制要求

参考

  • iOS view 布局详解1 - 通过设置view几何属性来实现布局
  • iOS view 布局详解2 - AutoLayout上篇
  • iOS view 布局详解3 - AutoLayout下篇
  • iOS view 布局详解4 - Masonry
  • Masonry介绍与使用实践(快速上手Autolayout)

你可能感兴趣的:(AutoLayout布局最佳实践)