概述

一般遇到两个或以上的控件进行一行或一列布局时,或队伍组合成卡片形式的布局时,运用 UIStackView 是最简略有用的计划,例如一些 tab 的展现时,可简略运用 UIStackView + UIScrollView 完成。当然要摆放的控件比较多,且需求分页加载的时分,请考虑运用 UICollectionView。

所以这儿的 Stack 不是堆栈的意思,也不存在压栈、弹栈的操作,可以了解为“堆叠”。UIStackView 完成了 UI 界面 X 轴和 Y 轴方向上的堆叠。类比了解,SwiftUI 带来的 ZStack 就是 Z 轴方向上的视图堆叠。

一图概之:

img

基本关键:

  • 基于 Auto Layout 布局子视图。UIStackView 本身也需求运用 Auto Layout 布局,运用 frame 布局或许作用不必定契合预期。
  • 开发者担任界说 UIStackView 的方位和尺度(可选),UIStackView 本身办理子视图的内容布局和本身巨细,即至少给 UIStackView 增加两个邻边的方位束缚。
  • 可动态修正一切特点。
  • 阉割了 UIView 基类的一些特性,如设置 backgroundColor

UIStackView 是 Apple 基于 Flexbox 思维来完成的布局,虽然是以一个控件(UIView 子类)呈现,但它做的更多是布局其加入子视图。这种布局思维更挨近物理国际的直觉。

Flexbox 在 2009 年被 W3C 提出,可以很简略、完整地完成各种页面布局,而且仍是呼应式的,开端被应用于前端领域,目前一切浏览器都已支撑。后来经过 React Native 和 Weex 等框架,它被带入到客户端开发中,一起支撑了 iOS 和 Android。想了解 Flexbox 的具体 CSS 布局,可参看 Flex 布局教程:语法篇 – 阮一峰的网络日志。要想更直观地体会把玩 Flexbox 布局,可参看以下链接:

  • display: flex
  • Flexbox Froggy – A game for learning CSS flexbox
  • Flexy Boxes — CSS flexbox playground and code generation tool

要想在 iOS 完整体会 Flexbox 布局,可运用 Texture 中的 ASStackLayoutSpec。一些聪明的开发者经过 UICollectionViewLayout 也完成了简略的 Flexbox 布局,如 UICollectionViewFlexboxLayout。

SwiftUI 也引入了些 Flexbox 布局,如 HStackVStackZStack,可参看 Building layouts with UIStackViews 简略体会其为布局带来的便当。

内容自适应规则

一句话概括:sub view content size + spacing

  • UIStackView 沿轴方向长度 = 一切摆放子视图巨细之和 + 子视图之间的距离总和
  • UIStackView 正交轴方向(笔直于轴方向)长度 = 最大摆放子视图的长度
  • isLayoutMarginsRelativeArrangementtrue,上述的长度还会包含相关的 layoutMargins

上面所说的长度都是拟合巨细(fitting size)。

注意:

这儿的子视图巨细是视图的 content size,内容巨细,是指 Auto Layout 束缚核算之后的 size,所以直接设置 frame 是无效的。有必要经过重写 intrinsicContentSize 特点或给子视图的宽高增加 Auto Layout 束缚。

这儿还隐含了一些潜规则:

  • 让 UIStackView 能自适应子视图巨细的前提是子视图要有 content size。
  • 终究子视图的 size 也不必定等于 content size,当 UIStackView 本身设置了宽高束缚,其会为了填充空间会对子视图进行拉伸或缩短。
  • 自适应子视图巨细意味着其不允许子视图溢出其本身。这与 CSS flexboxflex-wrap 表现有别。

别的 UIStackView 的这些布局特点会直接影响其自适应的巨细:

  • axis:界说了堆叠的轴方向,是在笔直方向仍是水平方向进行堆叠。
  • distribution:界说了轴方向上的子视图布局。
  • alignment:界说了轴正交方向上的子视图布局。
  • spacing:界说了轴方向上的子视图之间的最小距离
  • isBaselineRelativeArrangement:界说了视图之间的笔直距离是否从基线丈量。
  • isLayoutMarginsRelativeArrangement:界说了是否要基于子视图的 layoutMargins 来布局。

若修正上述特点无法到达你的预期作用,则优先查看 Xcode 操控台是否输出了 Auto Layout 束缚抵触的错误日志,从中查看需求修正的特点或补充的束缚。

调试 distributionalignment 在必定程度上还原调试 CSS 时捉襟见肘的体会。

NSLayoutConstraint.Axis

默以为 horizontal 水平方向。

img

UIStackView.Distribution

界说沿 UIStackView 轴方向的子视图的巨细和方位的布局。

除了 fillEqually 以外的 distribution,UIStackView 在沿轴方向核算尺度时,会运用每个子视图的 intrinsicContentSize 特点。而 fillEqually 会持平调整子视图的巨细,使其在轴方向的长度是一致的,假如或许, UIStackView 会拉伸一切子视图,以匹配轴方向最大内容巨细的视图。

fill

默许。UIStackView 调整其子视图的巨细,以填充轴方向上的可用空间。

当子视图塞不进 UIStackView 时,UIStackView 依据其抗压优先级(compression resistance priority)缩短视图。

当子视图没有塞满 UIStackView 时,UIStackView 依据其拥抱优先级(hugging priority)拉伸视图。

当存在歧义时,UIStackView 依据子视图在 arrangedSubviews 中的索引调整子视图的巨细。

img

上述说到的“抗压优先级”、“拥抱优先级”可参看 UIView 的这些 API 的相关资料:

func contentCompressionResistancePriority(for axis: NSLayoutConstraint.Axis) -> UILayoutPriority
func setContentCompressionResistancePriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis)
func contentHuggingPriority(for axis: NSLayoutConstraint.Axis) -> UILayoutPriority
func setContentHuggingPriority(_ priority: UILayoutPriority, for axis: NSLayoutConstraint.Axis)
fillEqually

UIStackView 调整其子视图的巨细,以填充轴方向上的可用空间。子视图会拉伸调整巨细(匹配最大的子视图),以坚持轴方向上的巨细都持平。

img

fillProportionally

UIStackView 调整其子视图的巨细,以填充轴方向上的可用空间。视图依据其沿 UIStackView 轴的内涵内容巨细按比例调整巨细。

img

equalSpacing

UIStackView 放置摆放子视图,以填充轴方向上的可用空间。当摆放的视图没有填充 UIStackView 时,UIStackView 会均匀地填充视图之间行距离。即此刻的 spaicing 只限制了最小的距离。

当子视图塞不进 UIStackView 时,UIStackView 会依据其抗压优先级缩短视图。

当存在歧义时,UIStackView 会依据其在 arrangedSubviews 中的索引缩短子视图。

img

equalCentering

对子视图中点等距布局,一起坚持子视图之间的距离。相同,此刻的 spaicing 只限制了最小的距离。

当子视图塞不进 UIStackView 时,UIStackView 缩短距离,直到到达 spaicing 值。若子视图仍塞不进 UIStackView,则会依据其抗压优先级缩短子视图。

当存在歧义时,UIStackView 会依据其在 arrangedSubviews 中的索引缩短子视图。

为了坚持子视图内容巨细,UIStackView 会突破中点等距布局。相同,为坚持子视图间的最小距离,UIStackView 会压缩子视图的内容巨细。

img

UIStackView.Alignment

界说笔直于 UIStackView 轴方向的子视图布局。

对于除 fill 之外的一切 alignment, UIStackView 在核算轴正交方向的巨细时运用每个子视图的 intrinsicContentSize 特点。fill 则调整一切子视图的巨细,以填充轴正交方向上的可用空间,假如或许, UIStackView 会拉伸一切子视图,以匹配轴正交方向上最大内涵巨细的视图。

fill

默许。 UIStackView 调整其子视图巨细,以填充轴正交方向上的可用空间。

img

center

UIStackView 把子视图中点沿轴对齐,即笔直方居中对齐。

img

leading:横轴时也可运用 top

UIStackView 把子视图沿前边际对齐。

img

img

trailing:横轴时也可运用 bottom

UIStackView 把子视图沿后边际对齐。

img

img

firstBaseline:仅横轴有用。

UIStackView 依据首个基线对齐摆放子视图。

img

lastBaseline:仅横轴有用。

UIStackView 依据末尾基线对齐摆放子视图。

img

距离

固定距离:

var spacing: CGFloat { get set }

默以为 0.0。此特点界说了在 UIStackViewDistribution.fill 子视图之间的严格距离,也是 UIStackView.Distribution.equalSpacingUIStackView.Distribution.equalCentering 的最小行距离。

运用负值会堆叠子视图,其堆叠层级按子视图的层级索引摆放。

更进一步,iOS 11.0+ 还增加了设置自界说距离的办法:

// Applies custom spacing after the specified view.
func setCustomSpacing(_ spacing: CGFloat, after arrangedSubview: UIView)
func customSpacing(after arrangedSubview: UIView) -> CGFloat

经过这个办法可设置 sub view 的自界说距离。但局限性也在这个“”字。如下图所示,spaicng 只能设置到 UIStackView 的 sub view 之间,而 UIStackView 的边际方位是不能设距离的。

暂时无法在飞书文档外展现此内容

这就需求一些小智慧了,可以直接给边际方位增加一个占位视图。这儿简略封装一个占位视图 SizeView

import UIKit
/// 自界说内容尺度视图
public class SizeView: UIView {
    public var size: CGSize = .zero
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    private func commonInit() {
        isUserInteractionEnabled = false
    }
    public override var intrinsicContentSize: CGSize {
        size
    }
}
public extension SizeView {
    convenience init(size: CGSize, color: UIColor? = nil) {
        self.init(frame: .zero)
        self.size = size
        backgroundColor = color
    }
    /// 尽或许大
    static func expanded(width: CGFloat = .greatestFiniteMagnitude, height: CGFloat = .greatestFiniteMagnitude, color: UIColor? = nil) -> SizeView {
        SizeView(size: CGSize(width: width, height: height), color: color)
    }
    /// 尽或许小
    static func shrinked(width: CGFloat = 0, height: CGFloat = 0, color: UIColor? = nil) -> SizeView {
        SizeView(size: CGSize(width: width, height: height), color: color)
    }
}

下面代码是音乐入口初始状况的布局代码:

noMusicStackView.then {
    let musicIconView = makeMusicIconView()
    // 运用占位视图增加左侧距离
    $0.addArrangedSubview(SizeView.shrinked(width: 12))
    $0.addArrangedSubview(musicIconView)
    // 运用自界说距离 API 设置 sub view 间的距离
    $0.setCustomSpacing(6, after: musicIconView)
    let addLabel = self.addLabel
    addLabel.font = UI.textFont
    addLabel.textColor = UI.textColor
    addLabel.layer.lv.setupShadow(color: UI.textShadow.color, offset: UI.textShadow.offset, radius: UI.textShadow.radius)
    $0.addArrangedSubview(addLabel)
    // 运用占位视图增加右侧距离
    $0.addArrangedSubview(SizeView.shrinked(width: 12))
}

作用:

imgimgimg

子视图办理

func addArrangedSubview(_ view: UIView)
var arrangedSubviews: [UIView] { get }
func insertArrangedSubview(_ view: UIView, at stackIndex: Int)
func removeArrangedSubview(_ view: UIView)

上述办法都会首要作用于 arrangedSubviews 数组。

调用 UIStackView 的 addArrangedSubview(_:) 时,增加的视图除了增加到 arrangedSubviews 中,一起也会增加到基类的 subviews 中,即成为子视图。

因为 UIStackView 内部会确保 arrangedSubviewssubviews 的子集,所以即使在调用 addArrangedSubview(_:) 前调用了基类的 addSubview(_:),也不会有什么影响,也不会改动其在 arrangedSubviews 中的顺序。但有必要要调用 addArrangedSubview(_:) 来增加办理的子视图,否则设置 UIStackView 的各个特点将无法作用于增加的子视图。

移除视图的时分要注意,removeArrangedSubview(_:) 仅仅从 arrangedSubviews 中移除子视图,即移除的子视图不受 UIStackView 办理,但其还在基类的 subviews 中,即还在视图层级中。所以要直接从层级中移除子视图,可直接运用基类的 removeFromSuperview() 办法。

布局办理

UIStackView 会动态呼应以下操作,并自动更新布局:

  1. 增加、删除或刺进到 arrangedSubviews
  2. 修正 UIStackView 界说的一切特点。
  3. 修正子视图的 isHidden 特点。其作用跟 UIView 对子视图的作用不一致,当值为 true 时 UIStackView 会从头核算布局(跟移除视图作用一致),还甚至默许增加了动画(轴方向缩短作用)!而 UIView 对子视图 isHiddentrue 时不会有布局更新,更不会有动画。

处理第 1 点办理 arrangedSubviews 的几个办法,第 2、3 点涉及的特点都可以增加动画!别的要操控子视图 isHidden 的时长,可以放到 animate(withDuration:animations:) 中操控。