背景
iphone 发布历史
- 在iphone1-iphone3gs时代 window的size固定为(320,480),我们只需要简单计算一下相对位置就好了
- 在iphone4-iphone4s时代 苹果推出了retina屏,但是给了码农们非常大的福利:window的size不变
- 在iphone5-iphone5s时代,window的size变了(320,568),这时autoresizingMask派上了用场(为啥这时候不用Autolayout? 因为还要支持ios5呗)
- 在iphone6+时代 window的width也发生了变化(相对5和5s的屏幕比例没有变化),终于是时候抛弃autoresizingMask改用autolayout了(不用支持ios5了 相对于屏幕适配的多样性来说autoresizingMask也已经过时了)
布局方式变革
纯手写代码所经历的关于页面布局的三个时期
MagicNumber -> autoresizingMask -> autolayout
- 直接设置view的
几何属性
, 最为常见的就是直接设置frame
- 通过直接对view几何属性值的设定到达期望的布局效果
- 不能自动适配布局的变化,MagicNumber
- 在1的基础上为view设置
AutoresizingMask
- 用于描述一个view的superview的大小发生改变时,这个view的布局改如何调整
- 最大的限制是autoresizingMask描述的变化特征只能限于view和其superview
-
Auto Layout
- 通过一系列的约束(constraints)来描述view间的布局关系, 系统会通过这些constraints来计算出view的几何属性
- 使用繁琐和啰嗦 ->
Masonry
Auto Layout 的基础概念
使用 Autolayout 的一般流程
- 添加约束之前必须将view添加到superview里
- 对于要使用Auto Layout的控件需要关闭Autoresizing
- 创建并添加约束
- 更新约束
约束(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上
Intrinsic Content Size
- 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
这个阶段包含了下面两个周期
-
Update constraints
之前提到过, 这个周期通过调用-setNeedsUpdateConstraints
来触发, 系统会调用-updateConstraints
这个方法, 我们可以重载这个方法来做一些更新约束相关的事情, 但在重载时要注意以下几点- 不要在这个方法里面做会让约束失效的事, 比如移除约束或是移除view
- 不要在这个方法里面调用跟Layout和Display周期相关的方法
- 一定要在方法的
最后
调用[super updateConstraints]
-
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支持的那些数值类型之外 就只支持 CGPoint
、CGSize
、 UIEdgeInsets
。
[初级] 让一个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));
*/
}];
这里有意思的地方是 and
和 with
其实这两个函数什么事情都没做。
但是用在这种链式语法中,就非常的巧妙和易懂。
- (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)