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 的嵌套:工具
在極簡狀況下,引入 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 } 複製代碼
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
,它依然在視圖結構中UIStackView 有幾個重要的屬性,這也是咱們惟一須要控制的開關,那解決一個頁面的佈局問題,就轉換成如何用這幾個有限的開關來描述這個頁面的元素。
horizontal
水平方向 (默認)vertical
垂直方向定義:
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壓縮
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
僅橫軸有用, 按文章底部基線對齊
設置元素之間的邊距值
決定了垂直軸若是是文本的話,是否按照 baseline 來參與佈局。
若是打開則經過 layout margins 佈局,關閉則經過 bounds
一、設置一個元素後面的邊距
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) } } } 複製代碼
UIStackView 的佈局會動態的同步數組 arrangedSubviews
的變化。 變化包括:
注意:對於隱藏(isHidden)的處理,UIStackView 會自動把空間利用起來,至關於暫時的刪去,而不像 Autolayout 通常不破壞約束的作法。
如何讓一層一層的 StackView 能夠和氣相處呢? 答案就是約束完備:
即使你目前正使用某種 Autolayout 的封裝,引入UIStackView 都是一個有效下降頁面約束複雜度的方式。它讓你能夠用一個大局觀去看待排版,而不是陷入每一個元素的約束細節裏。最棒的是,它提供了更低的維護成本(好比茫茫約束中插入一個按鈕)和更高的容錯率(手寫約束產生語義衝突)。
有同窗問實戰用起來是什麼感受。下面舉一個小例子:
這是一個有翻譯功能的聊天氣泡,只需關注深灰色的區域
stackView.addArrangedSubviews([contentLabel,
translationLoadingSeparatorLine,
translationLoadingView])
複製代碼
stackView.addArrangedSubviews([contentLabel,
translationResultTopSeparatorLine,
translationResultTextLabel,
translationResultBottomSeparatorLine,
translationResultBottomLabel])
複製代碼
切換一個頁面的佈局方案,就是清空和重裝對應的 stackView 就好了。 是否是優雅了一點?