前言
UIStackView 是 Apple 在 iOS9 推出的一套 API,它可以很好地減輕手動寫或拖 constraint 帶來的重複繁瑣的工作,也可以自動化的處理排列和元素個數的變化。
正由於其 iOS9+ 的門檻,而國內 app 普遍要相容 iOS8,再加上 UIStackView 的真正威力其實是 Storyboard, 即便有 FDStackView 這樣的黑科技可以降低引入門檻,團隊還是傾向於使用純 Masonry/SnapKit 的方式來實現 Autolayout。
UIStackView 顧名思義,就是一個檢視堆疊 ,換句話說:他是一個容器。這類容器型的控制元件我們不由聯想到 UITableView,UICollectionView。相比於這兩個傳統容器,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 的巢狀:
正題
1. 初始化
在極簡情況下,引入 UIStackView 的 view hierarchy 是一個這樣的狀況:
要實現這個簡單的模型,首先需要建立一個 UIStackView:
let stackView = UIStackView()
複製程式碼
然後把他加到父層的 UIView 上
view.addSubview(stackView)
複製程式碼
接著,把 子View 例項加到 UIStackView 裡,這裡呼叫的不是傳統的 addSubview
,而是
stackView.addArrangedSubview(subView1)
stackView.addArrangedSubview(subView2)
複製程式碼
這時 UIStackView 的 arrangedSubviews 就有值了
open var arrangedSubviews: [UIView] { get }
複製程式碼
arrangedSubviews
和 subviews
的順序意義是不同的:
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 resistance和hugging兩個 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
(預設) 儘可能鋪滿 -
.leading
當axis
是vertical
的時候,按 leading 方向對齊 等價於: 當axis
是horizontal
的時候,按 top 方向對齊 -
.top
當axis
是horizontal
的時候,按 top 方向對齊 等價於: 當axis
是vertical
的時候,按 leading 方向對齊 -
.trailing
當axis
是vertical
的時候,按 trailing 方向對齊 等價於: 當axis
是horizontal
的時候,按 bottom 方向對齊 -
bottom
當axis
是horizontal
的時候,按 bottom 方向對齊 等價於: 當axis
是vertical
的時候,按 trailing 方向對齊 -
.center
居中對齊 -
.firstBaseline
僅橫軸有用, 按首行基線對齊 -
.lastBaseline
僅橫軸有用, 按文章底部基線對齊
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])
複製程式碼
- 另一個是翻譯成功
stackView.addArrangedSubviews([contentLabel,
translationResultTopSeparatorLine,
translationResultTextLabel,
translationResultBottomSeparatorLine,
translationResultBottomLabel])
複製程式碼
切換一個頁面的佈局方案,就是清空和重灌對應的 stackView 就行了。 是不是優雅了一點?