UIStackView 入坑指南

前言

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

正因爲其 iOS9+ 的門檻,而國內 app 廣泛要兼容 iOS8,再加上 UIStackView 的真正威力實際上是 Storyboard, 即使有 FDStackView 這樣的黑科技能夠下降引入門檻,團隊仍是傾向於使用純 Masonry/SnapKit 的方式來實現 Autolayout。github

UIStackView 顧名思義,就是一個視圖堆棧 ,換句話說:他是一個容器。這類容器型的控件咱們不禁聯想到 UITableView,UICollectionView。相比於這兩個傳統容器,UIStackView 的定位是這樣的:swift

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

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

然而雖然說是處理佈局,但它也不能徹底代替 constraint,他能作的,很少很多,就是一個堆棧能作到的事,除此以外,好比 子View 的本身內在 size,或是 CHP(Content Hugging Priority),CRP(Content Resistance Priority),更包括 UIStackView 自己的佈局,都是離不開手寫約束。因此一個好的 Autolayout 封裝庫仍是須要的。markdown

要說其定位,應該就是介於 手寫約束 和 UITableView/UICollectionView 之間的工具。就像 iPad 是 筆記本電腦 和 手機 之間的設備同樣。它誰也代替不了,可是它有自信的領域,那就是手寫 Constraint 很累,可是用 UITableView/UICollectionView 又以爲很笨重的場合app

好比下面這個若是用原生實現,就能夠看作是這些 UIStackView 的嵌套:工具

正題

1. 初始化

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

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

let stackView = UIStackView()
複製代碼

而後把他加到父層的 UIView 上ui

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 佈局

  • .fillEqually 根據 等寬/高 佈局

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

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

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

2.3. alignment

定義

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

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

  • .fill (默認) 儘量鋪滿

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

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

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

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

  • .center 居中對齊

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

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

2.4. spacing

設置元素之間的邊距值

2.5. isBaselineRelativeArrangement(默認 false)

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

2.6. isLayoutMarginsRelativeArrangement (默認 false)

若是打開則經過 layout margins 佈局,關閉則經過 bounds

3. 自定義邊距能力

一、設置一個元素後面的邊距

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

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

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

三、獲取內部元素默認邊距

class let spacingUseDefault: CGFloat 複製代碼

四、獲取相鄰 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])
複製代碼

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

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

相關文章
相關標籤/搜索