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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容