专栏名称: darrenzheng
目录
相关文章推荐
51好读  ›  专栏  ›  darrenzheng

UIStackView 入坑指南

darrenzheng  · 掘金  ·  · 2019-01-04 05:59

正文

请到「今天看啥」查看全文


阅读 194

UIStackView 入坑指南

前言

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 都是一个有效降低页面 约束复杂度 的方式。它让你可以用一个 大局观 去看待排版,而不是陷入每个元素的约束细节里。最棒的是,它提供了更低的 维护成本 (比如茫茫约束中插入一个按钮)和更高的 容错率 (手写约束产生语义冲突)。







请到「今天看啥」查看全文