iOS布局方式总结

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实现动态margin

使用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)
        }
    }
UILayoutGuide实现多控件居中

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)
        }
    }
}
未使用safeAreaLayoutGuide,label范围超出安全区域

将 make.edges.equalToSuperview() 改成 make.edges.equalTo(self.safeAreaLayoutGuide)

//            make.edges.equalToSuperview()
            make.edges.equalTo(self.safeAreaLayoutGuide)
label范围在安全区域以内

5.1.7 可通过additionalSafeAreaInsets 修改VC的安全区域范围。

self.additionalSafeAreaInsets = UIEdgeInsets(top: 20.0, left: 50.0, bottom: 50.0, right: 50.0)
通过dditionalSafeAreaInsets缩小VC安全区域范围

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()
        }
    }
label显示在安全区域以内

UITableView 有个insetsContentViewsToSafeArea属性,会调整自动调整显示内容在安全区域以内,默认为true。

6. UIStackview
7. SDAutoLayout

你可能感兴趣的:(iOS布局方式总结)