1、What's Auto Layout
Auto Layout
是由苹果公司UIKit
框架提供的一个用于动态计算UIView
及其子类的大小和位置的库。
说到Auto Layout
就不得不说Cassowary
算法,因为Auto Layout
是构建在Cassowary
算法的基础之上的。1997年,Auto Layout
用到的布局算法论文发表,被称为高效的线性方程求解算法。2011年苹果利用Cassowary
算法为开发者提供了Auto Layout
自动布局库中。由于Cassowary
算法的本身的优秀,不仅是苹果公司,许多开发者将其运用到各个不同的开发语言中,如JavaScript、ASP.NET、Java、C++
等都有运用Cassowary
算法的库。从这里也可以看出Cassowary
算法自身的优秀和先进性,不然不会被运用的如此广泛。
苹果公司在iOS 6
系统时引入了Auto Layout
,但是直到现在已经更新到iOS 12
了,还有很多开发者还是不愿使用Auto Layout
。主要是对其反人类的语法以及对其性能问题的担忧。
针对Auto Layout
的一些问题,在iOS 9
发布时,苹果推出了更简洁语法的NSLayoutAnchor。同时发布了模仿前端Flexbox布局思路的UIStackView,以此为开发者在自动布局上提供更好的选择。
在苹果WWDC 2018 High Performance Auto Layout中苹果工程师说: iOS 12将大幅度提升Auto Layout性能,使滑动屏幕时达到满帧。
在WWDC 2018 What's New in Cocoa Touch苹果的工程师说了iOS 12对Auto Layout优化后的表现。
从图上可以看出,
iOS 11
中视图嵌套的数量的性能快成指数级别增长了,在iOS 12
中已经基本和手写frame布局的性能类似了。
从iOS 6
到iOS 12
,苹果也在不断的优化Auto Layout
的性能,同时为开发者提供更简洁的API
,如果你还在使用frame
手写布局,不妨试试Auto Layout
。下面我将介绍iOS
中几种常用的布局方法。
2、Auto Layout各个版本不同用法
如我要设置一个宽高为120,居中显示的View,效果如下图:
1、用frame手写布局
UIView *centerView = [[UIView alloc] init];
centerView.backgroundColor = [UIColor redColor];
[self.view addSubview:centerView];
CGFloat width = self.view.frame.size.width;
CGFloat height = self.view.frame.size.height;
[centerView setFrame:CGRectMake(width / 2 - (60), height / 2 - (60), 120, 120)];
2、iOS 6提供的NSLayoutConstraint语法添加约束
centerView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *consW = [NSLayoutConstraint constraintWithItem:centerView
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0
constant:120.0
];
NSLayoutConstraint *consH = [NSLayoutConstraint constraintWithItem:centerView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationEqual
toItem:self.view attribute:NSLayoutAttributeHeight
multiplier:0
constant:120.0
];
NSLayoutConstraint *consX = [NSLayoutConstraint constraintWithItem:centerView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0.0
];
NSLayoutConstraint *consY = [NSLayoutConstraint constraintWithItem:centerView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0.0
];
[self.view addConstraints:@[consW,consH,consX,consY]];
3、用VFL语法
centerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[centerView(120)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"[centerView(120)]" options:0 metrics:nil views:views]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:centerView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterY multiplier:1 constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:centerView attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
4、使用第三方开源框架Masonry或SnapKit
__weak typeof (self) weakSelf = self;
[centerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(120, 120));
make.center.equalTo(weakSelf.view);
}];
let centerView:UIView = UIView.init()
view.addSubview(centerView)
centerView.backgroundColor = UIColor.red
centerView.snp.makeConstraints { (make) in
make.width.equalTo(120)
make.height.equalTo(120)
make.center.equalTo(view)
}
5、使用iOS 9之后Apple提供的NSLayoutAnchor
let centerView:UIView = UIView.init()
view.addSubview(centerView)
centerView.backgroundColor = UIColor.red
centerView.translatesAutoresizingMaskIntoConstraints = false
centerView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
centerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true
centerView.widthAnchor.constraint(equalToConstant: 120).isActive = true
centerView.heightAnchor.constraint(equalToConstant: 120).isActive = true
通过上面的代码对比,使用frame
手写布局只要几行代码就搞定了,使用NSLayoutConstraint
语法和VFL
语法是最复杂的,尤其是NSLayoutConstraint
语法要用30多行代码才能是想同样的效果,代码行数越多出错的概率也就成正比上升,所以这就是很多开发者不愿使用Auto Layout
(或者说不愿意使用系统提供API来实现)的原因之一吧。
如果你的App
要兼容iOS 9
以下的各个版本,建议使用Masonry,如果只兼容iOS 9以上的版本,建议使用SnapKit或者系统提供的NSLayoutAnchor API,毕竟Masonry
这个库已经2年没有更新了。
在这里我推荐优先使用NSLayoutAnchor
,第三方的开源库随时都面临着一些问题:
-
iOS
系统版本的更新造成的适配和兼容问题,如果是开源代码要等到苹果发布新版本,代码的作者再做兼容和适配 - 代码的作者停止更新这些代码了,这对我们开发者来说就很被动了,我们要么自己修改这些代码,要么选择更新的开源代码
- 使用系统库可在打包时可以减少包大小
3、
Auto Layout
的生命周期
前面说到苹果的Auto Layout
是基于Cassowary
算法的,苹果在此基础上提供了一套Layout Engine
引擎,由它来管理页面的布局,来完成创建、更新、销毁等。
在APP
启动后,会开启一个常驻线程来监听约束变化,当约束发生变化后会出发Deffered Layout Pass
(延迟布局传递),在里面做容错处理(如有些视图在更新约束时没有确定或缺失布局申明),完成后进入约束监听变化的状态。
当下一次刷新视图(如调用layoutIfNeeded()
)时,Layout Engine
会从上到下调用layoutSubviews()
,然后通过Cassowary
算法计算各个子视图的大小和位置,算出来后将子视图的frame
从layout Engine
里拷贝出来,在之后的处理就和手写frame
的绘制、渲染的过程一样了。使用Auto Layout
和手写frame
多的工作就在布局计算上。
4、
NSLayoutAnchor
常用属性
- leadingAnchor
- trailingAnchor
- leftAnchor
- rightAnchor
- topAnchor
- bottomAnchor
- widthAnchor
- heightAnchor
- centerXAnchor
- centerYAnchor
- firstBaselineAnchor
- lastBaselineAnchor
对于NSLayoutAnchor
的一些常用属性,通过其命名就能看出来其作用,这里不做赘述,如果想了解更多请查阅Apple Developer NSLayoutAnchor。
5、Auto Layout几个更新约束的方法
setNeedsLayout: 告知页面需要更新,但是不会立刻开始更新。执行后会立刻调用
layoutSubviews
。layoutIfNeeded: 告知页面布局立刻更新。所以一般都会和
setNeedsLayout
一起使用。如果希望立刻生成新的frame
需要调用此方法,利用这点一般布局动画可以在更新布局后直接使用这个方法让动画生效。layoutSubviews: 更新子
View
约束setNeedsUpdateConstraints:需要更新约束,但是不会立刻开始
updateConstraintsIfNeeded:立刻更新约束
updateConstraints:更新
View
约束
6、
NSLayoutAnchor
使用注意事项
- 1、在使用
NSLayoutAnchor
为视图添加约束时一定要先把translatesAutoresizingMaskIntoConstraints
设置false
centerView.translatesAutoresizingMaskIntoConstraints = false
- 2、在使用
safeAreaLayoutGuide
适配iPhone X
等机型时要对iOS 11
之前的系统做适配,否则会导致低版本系统上程序Crash
if #available(iOS 11.0, *) {
tableView.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
} else {
tableView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0).isActive = true
}
- 3、设置约束后要将其激活,即设置
isActive
为true
let centerX: NSLayoutConstraint = centerView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
centerX.isActive = true
- 4、
leadingAnchor
不要和leftAnchor
混用
centerView.leadingAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
centerView.leftAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
以上2种写法,在编译时不会出现任何问题,但是在运行时就会报错,并会导致程序Crash,官方的说法是:
While the NSLayoutAnchor class provides additional type checking, it is still possible to create
invalid constraints. For example, the compiler allows you to constrain one view’s leadingAnchor
with another view’s leftAnchor, since they are both NSLayoutXAxisAnchor instances. However,
Auto Layout does not allow constraints that mix leading and trailing attributes with left or right
attributes. As a result, this constraint crashes at runtime.
同理,trailingAnchor
和rightAnchor
也不能混用。
- 5、如何刷新某个约束
如我要修改一个UIView
的宽度:
通过代码添加约束,可把UIView
的宽度设置类属性,然后在需要的地方修改constant
的参数,然后在刷新约束即可,代码如下:
var centerView: UIView!
var centerWidth: NSLayoutConstraint!
self.centerView = UIView.init()
view.addSubview(self.centerView)
self.centerView.backgroundColor = UIColor.red
self.centerView.translatesAutoresizingMaskIntoConstraints = false
self.centerView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0).isActive = true
self.centerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true
self.centerWidth = self.centerView.widthAnchor.constraint(equalToConstant: 120)
self.centerWidth.isActive = true
self.centerView.heightAnchor.constraint(equalToConstant: 120).isActive = true
self.centerWidth.constant = 250
UIView.animate(withDuration: 0.35, animations: { [weak self] in
guard let `self` = self else { return }
self.centerView.superview?.layoutIfNeeded()
}) { (finished) in
}
效果如下:
如果是xib
或者storyboard
,那就更简单了,直接摁住键盘control
键,拖到对应的类里,然后在需要的地方修改约束并刷新即可。操作如下:
- 6、设置宽高比
在开发中,我们会遇到一些需求要求根据UIView
的宽高比来设置约束,如一般情况下显示视频的宽高比是16:9,通过代码设置宽高比如下:
centerView.heightAnchor.constraint(equalToConstant: 90).isActive = true
centerView.widthAnchor.constraint(equalTo: centerView.heightAnchor, multiplier: 16 / 9).isActive = true
7、
Auto Layout
自适应UITableViewCell
高度使用
- 使用
rowHeight
设置高度
一般情况下,如果UITableView
的每个Cell
高度是固定的我们可以直接指定一个值即可,如果没有设置UITableView
的高度,系统会默认设置rowHeight
高度是44。
tableview.rowHeight = 44;
也可以通过UITableViewDelegate的代理来设置UItableView的高度。
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50
}
如果通过手动计算每个UItableViewCell
的高度,也在这个代理中实现,通过计算返回每个UItableViewCell
的高度。
- 使用
estimatedRowHeight
设置高度
UItableView
继承自UIScrollView
,UIScrollView
的滚动需要设置其contentSize
后,然后根据自身的bounds、contentInset、contentOffset
等属性来计算出可滚动的长度。而UITableView
在初始化时并不知道这些参数,只有在设置了delegate
和dataSource
之后,根据创建的UITableViewCell
的个数和加载的UITableViewCell
的高度之后才能算出可滚动的长度。
在使用Auto Layout
自适应UITableViewCell
高度时应提前设置一个估算值,当然这个估算值越接近真实值越好。
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 200
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return 200
}
如上图所示:这个界面就是用Auto Layout + estimatedRowHeight
完成自适应高度的,在添加约束时要按照从上到下的书讯设置每一个UIView
的顶部(top
)到上一个的视图底部的(bottom
)距离,同时要计算UITableViewCell
内部所有控件的高度。那么问题来了,用户发布的内容详情没有得到数据之前时没办法算出其高度的,此处可以先给内容文字UILabel
设置一个默认高度,然后让其根据内容填充自动计算高度:
topicInfoLab.heightAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true;
topicInfoLab.font = UIFont.init(name: "Montserrat-SemiBold", size: 12)
topicInfoLab.numberOfLines = 0
如果用户发布内容没有图片,直接设置发布内容UILabel距离UITableView距离底部的约束距离即可;
detailsLab.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8).isActive = true
如果用户发布的内容有图片,那么在计算出每张图片的位置和大小之后,一定要给最后一张图片设置距离UItableViewCell
底部(bottom
)的约束距离。
for(idx, obj) in imageArray.enumerated() {
//.....计算图片的大小和位置
if idx == imageArray.count - 1 {
//设置最后一张图片距离底部的约束
photo.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8).isActive = true
}
}
实现思路如上图所示,具体实现的请看代码
8、
Compression Resistance Priority
和Hugging Priority
使用
Compression Resistance Priority
和 Hugging Priority
在实际使用中往往配合使用,分别处理在同义水平线上多个view之间内容过少和内容过多而造成的互相压挤的情况。
Hugging Priority
的意思就是自包裹的优先级,优先级越高,则优先将尺寸按照控件的内容进行填充。
Compression Resistance Priority
,意思是说当不够显示内容时,根据这个优先级进行切割。优先级越低,越容易被切掉。
ContentHuggingPriority |
表示当前的UIView 的内容不想被拉伸 |
---|---|
ContentCompressionResistancePriority |
表示当前的UIView 的内容不想被收缩 |
默认情况下: HuggingPriority = 250 |
默认情况下: CompressionResistancePriority = 750 |
如设置2个UILabel
的拉伸优先级可使用代码:
fristLab.setContentHuggingPriority(UILayoutPriority(rawValue: 251), for: .horizontal)
secondLab.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 750), for: .horizontal)
9、总结
本文主要分享了苹果Auto Layout
的几种实现方法和注意事项,对于Auto Layout
在实际开发中的使用是采用纯代码、还是xib
+ 代码,还是storyboard
+ 代码,还是xib
+ storyboard
+ 代码的方式实现,主要看团队的要求、个人的习惯,以及App
的繁琐程度。
对于Auto Layout
在视图上的使用,个人建议如果UI比较简单或者单一的界面可使用Auto Layout
,如果UI的操作或刷新很复杂的界面,建议还是frame
+ 手动布局的方式。
本文demo,请戳这里
本文参考:
深入剖析Auto Layout,分析iOS各版本新增特性
Auto Layout 是怎么进行自动布局的,性能如何?
Apple Developer High Performance Auto Layout
Apple Develope NSLayoutConstraint
WWDC 2018 What's New in Cocoa Touch