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 就好了。 是否是优雅了一点?

相关文章
相关标签/搜索