UIStackView 入坑指南

darrenzheng發表於2019-01-04

前言

UIStackView 是 Apple 在 iOS9 推出的一套 API,它可以很好地減輕手動寫或拖 constraint 帶來的重複繁瑣的工作,也可以自動化的處理排列元素個數的變化。

正由於其 iOS9+ 的門檻,而國內 app 普遍要相容 iOS8,再加上 UIStackView 的真正威力其實是 Storyboard, 即便有 FDStackView 這樣的黑科技可以降低引入門檻,團隊還是傾向於使用純 Masonry/SnapKit 的方式來實現 Autolayout。

UIStackView 顧名思義,就是一個檢視堆疊 ,換句話說:他是一個容器。這類容器型的控制元件我們不由聯想到 UITableView,UICollectionView。相比於這兩個傳統容器,UIStackView 的定位是這樣的:

UIStackView 入坑指南

  • 容易編寫
  • 容易維護
  • 方便組合疊加
  • 輕量

UIStackView 和傳統容器類另一個區別是他自己雖然繼承自 UIView,但它本身不能自我渲染,比如他的 backgroundColor 是無效的,所以它註定要和 UIView 相輔相成的進行工作。它能夠幫助 UIView 來處理 子View 的位置和大小等佈局問題。

然而雖說是處理佈局,但它也不能完全代替 constraint,他能做的,不多不少,就是一個堆疊能做到的事,除此之外,比如 子View 的自己內在 size,或是 CHP(Content Hugging Priority),CRP(Content Resistance Priority),更包括 UIStackView 本身的佈局,都是離不開手寫約束。所以一個好的 Autolayout 封裝庫還是需要的。

要說其定位,應該就是介於 手寫約束 和 UITableView/UICollectionView 之間的工具。就像 iPad 是 膝上型電腦 和 手機 之間的裝置一樣。它誰也代替不了,但是它有自信的領域,那就是手寫 Constraint 很累,但是用 UITableView/UICollectionView 又覺得很笨重的場合

比如下面這個如果用原生實現,就可以看做是這些 UIStackView 的巢狀:

UIStackView 入坑指南

正題

1. 初始化

在極簡情況下,引入 UIStackView 的 view hierarchy 是一個這樣的狀況:

UIStackView 入坑指南

要實現這個簡單的模型,首先需要建立一個 UIStackView:

let stackView = UIStackView()
複製程式碼

然後把他加到父層的 UIView 上

view.addSubview(stackView)
複製程式碼

接著,把 子View 例項加到 UIStackView 裡,這裡呼叫的不是傳統的 addSubview,而是

stackView.addArrangedSubview(subView1)
stackView.addArrangedSubview(subView2)
複製程式碼

這時 UIStackView 的 arrangedSubviews 就有值了

open var arrangedSubviews: [UIView] { get }
複製程式碼

arrangedSubviewssubviews 的順序意義是不同的:

  • subviews:它的順序實際上是圖層覆蓋順序,也就是檢視元素的 z軸
  • arrangedSubviews:它的順序代表了 stack 堆疊的位置順序,即檢視元素的x軸和y軸

實戰中,我用這樣一個擴充套件來批量新增:

extension UIStackView {
    func addArrangedSubviews(_ views: [UIView?]) {
            views.compactMap({ $0 }).forEach { addArrangedSubview($0) }
    }
}
複製程式碼

既然 UIStackView 是 UIView,意味著即可以呼叫 addSubview,也可以 addArrangedSubview,他們的關係是什麼樣的呢?

  • 如果一個元素沒有被 addSubview,呼叫 arrangedSubviews 會自動 addSubview
  • 當一個元素被 removeFromSuperview ,則 arrangedSubviews也會同步移除
  • 當一個元素被 removeArrangedSubview, 不會觸發 removeFromSuperview,它依然在檢視結構中

2. 控制佈局的方式

UIStackView 有幾個重要的屬性,這也是我們唯一需要控制的開關,那解決一個頁面的佈局問題,就轉換成如何用這幾個有限的開關來描述這個頁面的元素。

2.1. axis 軸

  • horizontal 水平方向 (預設)
  • vertical 垂直方向

2.2. distribution 分佈

定義:

The layout that defines the size and position of the arranged views along the stack view’s axis.

描述和 axis 方向一致的元素之間的佈局關係

  • .fill (預設) 根據compression resistancehugging兩個 priority 佈局

    UIStackView 入坑指南

  • .fillEqually 根據 等寬/高 佈局

    UIStackView 入坑指南

  • .fillProportionally 根據intrinsic content size按比例佈局

    UIStackView 入坑指南

  • equalSpacing 等間距佈局,如果放不下,根據compression resistance壓縮

    UIStackView 入坑指南

  • .equalCentering 等中間線間距佈局,元素間距不小於 spacing 定義的值, 如果放不下,根據compression resistance壓縮

    UIStackView 入坑指南

2.3. alignment

定義

The alignment of the arranged subviews perpendicular to the stack view’s axis.

描述和 axis 垂直的元素之間的佈局關係

  • .fill (預設) 儘可能鋪滿

    UIStackView 入坑指南

  • .leadingaxisvertical 的時候,按 leading 方向對齊 等價於: 當 axishorizontal 的時候,按 top 方向對齊

    UIStackView 入坑指南

  • .topaxishorizontal 的時候,按 top 方向對齊 等價於: 當 axisvertical 的時候,按 leading 方向對齊

    UIStackView 入坑指南

  • .trailingaxisvertical 的時候,按 trailing 方向對齊 等價於: 當 axishorizontal 的時候,按 bottom 方向對齊

    UIStackView 入坑指南

  • bottomaxishorizontal 的時候,按 bottom 方向對齊 等價於: 當 axisvertical 的時候,按 trailing 方向對齊

    UIStackView 入坑指南

  • .center 居中對齊

    UIStackView 入坑指南

  • .firstBaseline 僅橫軸有用, 按首行基線對齊

    UIStackView 入坑指南

  • .lastBaseline 僅橫軸有用, 按文章底部基線對齊

    UIStackView 入坑指南

2.4. spacing

設定元素之間的邊距值

2.5. isBaselineRelativeArrangement(預設 false)

決定了垂直軸如果是文字的話,是否按照 baseline 來參與佈局。

2.6. isLayoutMarginsRelativeArrangement (預設 false)

如果開啟則通過 layout margins 佈局,關閉則通過 bounds

3. 自定義邊距能力

1、設定一個元素後面的邊距

func setCustomSpacing(_ spacing: CGFloat, 
	      after arrangedSubview: UIView)
複製程式碼

2、獲取一個元素後面的邊距

func customSpacing(after arrangedSubview: UIView) -> CGFloat
複製程式碼

3、獲取內部元素預設邊距

class let spacingUseDefault: CGFloat
複製程式碼

4、獲取相鄰 View 之間的預設邊距

class let spacingUseSystem: CGFloat
複製程式碼

但是需要注意的是,自定義邊距是 iOS11+ 的特性,如果需要 iOS9 相容, 需要引入一個hack的方案

extension UIStackView {
    // How can I create UIStackView with variable spacing between views?
    func addCustomSpacing(_ spacing: CGFloat, after arrangedSubview: UIView) {
        if #available(iOS 11.0, *) {
            self.setCustomSpacing(spacing, after: arrangedSubview)
        } else {
            let separatorView = UIView(frame: .zero)
            separatorView.translatesAutoresizingMaskIntoConstraints = false
            switch axis {
            case .horizontal:
                separatorView.widthAnchor.constraint(equalToConstant: spacing).isActive = true
            case .vertical:
                separatorView.heightAnchor.constraint(equalToConstant: spacing).isActive = true
            }
            if let index = self.arrangedSubviews.firstIndex(of: arrangedSubview) {
                insertArrangedSubview(separatorView, at: index + 1)
        }
    }
}
複製程式碼

4. 處理佈局變化

UIStackView 的佈局會動態的同步陣列 arrangedSubviews 的變化。 變化包括:

  • 追加
  • 刪除
  • 插入
  • 隱藏

注意:對於隱藏(isHidden)的處理,UIStackView 會自動把空間利用起來,相當於暫時的刪去,而不像 Autolayout 一般不破壞約束的做法。

5. 巢狀

如何讓一層一層的 StackView 可以和睦相處呢? 答案就是約束完備

  • 保證 父View 上的佈局是一個靈活佈局,比如需要拉伸的 View 就不要定死寬或高
  • 如果定死了尺寸,則 CHP、CRP 也無法解決問題
  • 保證 子View 可以正確算出自己的 intrinsic size

結語

即便你目前正使用某種 Autolayout 的封裝,引入UIStackView 都是一個有效降低頁面約束複雜度的方式。它讓你可以用一個大局觀去看待排版,而不是陷入每個元素的約束細節裡。最棒的是,它提供了更低的維護成本(比如茫茫約束中插入一個按鈕)和更高的容錯率(手寫約束產生語義衝突)。

----- 1月7日更新 ----

有同學問實戰用起來是什麼感覺。下面舉一個小例子:

這是一個有翻譯功能的聊天氣泡,只需關注深灰色的區域

  • 一個暫態是翻譯中
stackView.addArrangedSubviews([contentLabel,
                               translationLoadingSeparatorLine,
                               translationLoadingView])
複製程式碼

UIStackView 入坑指南

  • 另一個是翻譯成功
stackView.addArrangedSubviews([contentLabel,
                               translationResultTopSeparatorLine,
                               translationResultTextLabel,
                               translationResultBottomSeparatorLine,
                               translationResultBottomLabel])
複製程式碼

UIStackView 入坑指南

切換一個頁面的佈局方案,就是清空和重灌對應的 stackView 就行了。 是不是優雅了一點?

相關文章