1. frame布局。
性能相对比较好,但当views比较多,view依赖关系比较复杂或适配不同机型时,处理起来会比较繁琐,代码可读性低。特别在数据变化或横竖屏切换导致界面布局变化,通常要重新计算每个视图的frame,工作量巨大。
2. autoresizing布局。通过设置UIView的autoresizingMask属性来设置布局方式组合。
缺点:描述界面变化规则不够灵活,很多变化规则根本无法精确描述。
变化规则只能基于父视图与子视图之间,无法建立同级视图或者跨级视图之间的关系。
3. Auto Layout(NSLayoutConstraint)。
Cassowary的布局算法,通过将布局问题抽象成线性不等式,并分解成多个位置间的约束, Apple 在iOS 6推出的 Auto Layout(NSLayoutConstraint),内部使用的是该算法。
NSLayoutConstraint包含firstItem(约束视图),secondItem(参照视图),firstAttribute(约束视图的属性),secondAttribute(参照视图的属性),relation(关系,包括>=,=,<=), multiplier(比例系数),constant(常量),priority(优先级)。
约束属性中NSLayoutAttributeBaseline代表相对基线对齐。比如在UILabel中,基线是文字底部的位置,相对bottom略高。在大部分view中,基线和底部是一致的。
iOS 8对约束属性增加了一系列带上Margin的布局属性,类似CSS里的padding,比如NSLayoutAttributeLeftMargin。相对于NSLayoutAttributeLeft的左对齐,NSLayoutAttributeLeftMargin一般会在左边留出8个距离作为margin,可通过layoutMargins属性修改。
priority优先级只有在两个约束有冲突的时候才起作用,优先级高的会覆盖优先级低的,最高的优先级为1000。
3.1 translatesAutoresizingMaskIntoConstraints。
UIView有个translatesAutoresizingMaskIntoConstraints属性,对于用代码创建的view,默认值是true。translatesAutoresizingMaskIntoConstraints会将 frame/autoresizing布局 自动转化为 auto layout布局,转化的结果是为这个视图自动添加所有需要的约束,如果我们这时给视图添加自己创建的约束就一定会约束冲突。为了避免约束冲突,需要设置translatesAutoresizingMaskIntoConstraints = false。
3.2 UILayoutGuide。
如果要实现布局 对多个view之间的magin动态约束(margin的值不是固定,值受到布局约束),或者实现多控件共同居中,一种常见的实现方式是使用一个或多个辅助view,专门用于实现它们的约束关系。但这种辅助view会增加view视图复杂度,并会加入到事件响应路由中。iOS 9 便推出了UILayoutGuide来代替这种辅助view,UILayoutGuide直接继承自NSObject,并没有真正的创建一个View,只是创建了一个矩形空间,只在进行auto layout时参与进来计算。
3.2 safeAreaLayoutGuide(继承自UILayoutGuide)。
iOS 11 增加了safeAreaLayoutGuide 和 safeAreaInsets作为UIView的安全区属性。safeAreaLayoutGuide用于自动布局下对子视图建立与安全区域的约束,safeAreaInsets用于frame布局,返回view四个方向与安全区域的偏移量。safeAreaInsets在viewDidLoad获取不到真实的值,可以在viewSafeAreaInsetsDidChange获取。
4. NSLayoutAnchor。iOS 9 推出的自动布局类,通过设置view的不同锚来实现自动布局约束,内部可以理解成也是NSLayoutConstraint实现。NSLayoutAnchor相对NSLayoutConstraint,代码更加整洁,优雅,易读。
4. VFL。Visual Format Language 可视化格式语言是苹果公司为了简化Autolayout的编码而推出的抽象语言。通过一个抽象后的字符串描述视图的自动布局约束,简化了代码,增加了可读性。
5. 自动布局SnapKit/Masonry。主流使用的自动布局框架,它们使用链式编程的方式对NSLayoutConstraint进行了二次封装。举个例子:
make.bottom.lessThanOrEqualTo(contentView.snp.bottom).multipliedBy(0.5).offset(-10). priority(.low)
可以理解成NSLayoutConstraint的如下伪代码。
firstItem.firstAttribute.relation(secondItem. secondAttribute). multiplier. constant.priority
从snapKit源码可以得知,SnapKit会自动将view的translatesAutoresizingMaskIntoConstraints设置为false。对于使用了snapKit的view,关闭布局向auto layout隐式转换。
extension LayoutConstraintItem {
internal func prepare() {
if let view = self as? ConstraintView {
view.translatesAutoresizingMaskIntoConstraints = false
}
}
}
5.1高级用法汇总:
5.1.1 对单个约束进行操作。
var labelConstraint: Constraint?
label.snp.makeConstraints { (make) in
make.top.equalToSuperview()
make.right.lessThanOrEqualToSuperview()
labelConstraint = make.right.lessThanOrEqualTo(button.snp.left).constraint
}
// 关闭约束
labelConstraint?.deactivate()
// 开启约束
labelConstraint?.activate()
// 更新约束
labelConstraint?.update(offset: -10)
// 更改优先级
labelConstraint?.update(priority: .low)
5.1.2 contentHuggingPriority 和 ContentCompressionResistancePriority。
UILabel、UIImageView、UIButton 在没有设置size约束的时候,会使用数据填充计算后的intrinsicContentSize作为视图的size约束。contentHuggingPriority(拒绝放大优先级) 和 ContentCompressionResistancePriority(拒绝压缩优先级)常用于多个使用intrinsicContentSize作为自身size约束的视图,在相互存在水平或垂直方向关联约束,导致视图需要压缩或放大的拒绝优先级,拒绝优先级低的视图优先放大/压缩。
在使用拒绝压缩优先级时,若要指定视图满足最小宽度,此时在极限情况,所有视图都会出现压缩,因此需要将宽度优先级设置最高(大于所有的缩小优先级)
let label1 = UILabel()
label1.text = "111111111111111111111111111111111111111"
view.addSubview(label1)
let label2 = UILabel()
label2.text = "222222222222222222222222222222222222222222"
view.addSubview(label2)
label1.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label2.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
label1.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.top.equalTo(50)
// label1宽度优先级大于label2的压缩优先级
make.width.greaterThanOrEqualTo(50).priority(.required)
}
label2.snp.makeConstraints { (make) in
make.right.equalToSuperview()
make.top.equalTo(50)
make.left.equalTo(label1.snp.right)
}
5.1.3 UILayoutGuide。
使用 UILayoutGuide 作为虚拟占位布局对象,可以实现多控件居中,动态margin等约束效果,同3.2。
使用UILayoutGuide实现动态margin,三等分间距效果:
func test() {
let blueView = UIView()
blueView.backgroundColor = .blue
view.addSubview(blueView)
let redView = UIView()
redView.backgroundColor = .red
view.addSubview(redView)
let leftLayoutGuide = UILayoutGuide()
let middleLayoutGuide = UILayoutGuide()
let rightLayoutGuide = UILayoutGuide()
view.addLayoutGuide(leftLayoutGuide)
view.addLayoutGuide(middleLayoutGuide)
view.addLayoutGuide(rightLayoutGuide)
blueView.snp.makeConstraints { (make) in
make.height.width.equalTo(50)
make.top.equalTo(100)
}
redView.snp.makeConstraints { (make) in
make.height.width.equalTo(50)
make.top.equalTo(100)
}
leftLayoutGuide.snp.makeConstraints { (make) in
make.left.equalToSuperview()
make.right.equalTo(blueView.snp.left)
}
middleLayoutGuide.snp.makeConstraints { (make) in
make.left.equalTo(blueView.snp.right)
make.right.equalTo(redView.snp.left)
make.width.equalTo(leftLayoutGuide)
}
rightLayoutGuide.snp.makeConstraints { (make) in
make.right.equalToSuperview()
make.left.equalTo(redView.snp.right)
make.width.equalTo(leftLayoutGuide)
}
}
使用UILayoutGuide实现多控件居中:
func test() {
let blueView = UIView()
blueView.backgroundColor = .blue
view.addSubview(blueView)
let redView = UIView()
redView.backgroundColor = .red
view.addSubview(redView)
let layoutGuide = UILayoutGuide()
view.addLayoutGuide(layoutGuide)
blueView.snp.makeConstraints { (make) in
make.height.equalTo(50)
make.width.equalTo(100)
make.top.equalTo(100)
}
redView.snp.makeConstraints { (make) in
make.height.width.equalTo(50)
make.top.equalTo(100)
make.left.equalTo(blueView.snp.right).offset(20)
}
layoutGuide.snp.makeConstraints { (make) in
make.centerX.equalToSuperview()
make.left.equalTo(blueView.snp.left)
make.right.equalTo(redView.snp.right)
}
}
5.1.4 在父视图高度不确定,受数据填充和多个子视图布局影响。可以通过对多个可能的底部视图分别设定make.bottom.lessThanOrEqualTo/make.bottom.lessThanOrEqualToSuperview(),实现父视图动态高度。
5.1.5 对父视图调用layoutIfNeeded()使约束立即生效(自身调用只有size生效),可在动画中使用产生约束动画。
label1.superview?.setNeedsLayout()
UIView.animate(withDuration: 2) {
label1.snp.updateConstraints { (make) in
make.top.equalTo(200)
}
label1.superview?.layoutIfNeeded()
}
5.1.6 使用safeAreaLayoutGuide属性,将视图放在安全区域内。
func test() {
let redView = UIView()
redView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
view.addSubview(redView)
redView.snp.makeConstraints { (make) in
make.edges.equalTo(self.view)
}
let blueView = UIView()
blueView.backgroundColor = UIColor.blue.withAlphaComponent(0.5)
view.addSubview(blueView)
blueView.snp.makeConstraints { (make) in
make.edges.equalTo(self.view.safeAreaLayoutGuide)
}
}
即使不是VC的视图,获取的safeAreaLayoutGuide也是在安全区域中。
func test() {
let testView = TestView()
view.addSubview(testView)
testView.snp.makeConstraints { (make) in
make.left.right.equalTo(self.view.safeAreaLayoutGuide)
make.top.equalToSuperview()
make.height.equalTo(120)
}
}
class TestView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupUI() {
backgroundColor = .gray
let label = UILabel.init()
label.numberOfLines = 0
label.text = "123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123123"
addSubview(label)
label.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
// make.edges.equalTo(self.safeAreaLayoutGuide)
}
}
}
将 make.edges.equalToSuperview() 改成 make.edges.equalTo(self.safeAreaLayoutGuide)
// make.edges.equalToSuperview()
make.edges.equalTo(self.safeAreaLayoutGuide)
5.1.7 可通过additionalSafeAreaInsets 修改VC的安全区域范围。
self.additionalSafeAreaInsets = UIEdgeInsets(top: 20.0, left: 50.0, bottom: 50.0, right: 50.0)
UIView的insetsLayoutMarginsFromSafeArea属性默认为true,代表layoutMargin属性会加上safeArea,设为false,则不会加上safeArea。
5.1.8 UIScrollView 中的 safe area。
在iOS 11以前,当automaticallyAdjustsScrollViewInsets属性为true,导航栏为半透明,VC的加入的第一个scrollView会自动调整其contentInset,以保证滑动视图里的内容不被UINavigationBar与UITabBar遮挡。contentInset是实际的inset。
在iOS 11或以后,取代成UIScrollView的contentInsetAdjustmentBehavior属性,当scrollView超出安全区域,会调整inset以防止scrollView的内容超出安全区域。contentInset 是用户自定义的inset,adjustedContentInset是实际的inset,并且是只读属性。可以理解成 contentInset + contentInsetAdjustmentBehavior调整的inset = adjustedContentInset(实际inset)。
func test() {
scrollView.backgroundColor = .purple
scrollView.contentInsetAdjustmentBehavior = .always
view.addSubview(scrollView)
scrollView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
let label = UILabel()
label.text = "123123123123"
label.textColor = .white
scrollView.addSubview(label)
label.snp.makeConstraints { (make) in
make.left.top.equalToSuperview()
}
}
UITableView 有个insetsContentViewsToSafeArea属性,会调整自动调整显示内容在安全区域以内,默认为true。