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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容