iOS-UIStackView使用指南

UIStackView使用指南

概述

一般遇到两个或以上的控件进行一行或一列布局时,或行列组合成卡片形式的布局时,使用 UIStackView 是最简单有效的方案,例如一些 tab 的展示时,可简单使用 UIStackView + UIScrollView 实现。当然要排列的控件比较多,且需要分页加载的时候,请考虑使用 UICollectionView

所以这里的 Stack 不是堆栈的意思,也不存在压栈、弹栈的操作,可以理解为“堆叠”。UIStackView实现了U界面×轴和丫轴方向上的堆叠。类比理解,Swiftul 带来的 ZStack 就是Z轴方向上的视图堆叠。

一图概之:

基本要点:

  • 基于 Auto Layout 布局子视图。UIStackView 自身也需要使用 Auto Layout 布局,使用 frame 布局可能效果不一定符合预期。
  • 开发者负责定义 UIStackView 的位置和尺寸(可选),UIStackView 自身管理子视图的内容布局和自身大小,即至少给 UIStackView 添加两个邻边的位置约束。
  • 可动态修改所有属性。
  • 阄割了 UView 基类的一些特性,如设置 backgroundColor

UIStackView 是 Apple 基于 Flexbox 思想来实现的布局,虽然是以一个控件 (UlView 子类)呈现,但它做的更多是布局其加入子视图。这种布局思想更接近物理世界的直觉。

Flexbox 在2009 年被W3C 提出,可以很简单、完整地实现各种页面布局,而且还是响应式的,开始被应用于前端领域,目前所有浏览器都已支持。后来通过 React Native 和 Weex等框架,它被带入到客户端开发中,同时支持了 i0s 和 Android。想了解 Flexbax 的详细 CSS布局,可参阅 Flex 布局教程:语法篇-阮一峰的网络日志

要想更直观地体验把玩 Flexbox 布局,可参阅以下链接:

要想在iOS完整体验 Flexbox 布局,可使用 Texture 中的 ASStackLayoutSpec。一些聪明的开发者通过UICollectionViewLayout也实现了简单的 Flexbox 布局,如 UICollectionViewFlexboxLayouto SwiftUl也引入了些 Flexbox 布局,如 HStack、 vStack 和 ZStack,可参阅 Building layouts with UIStackViews 简单体验其为布局带来的便利

内容自适应规则

一句话概括:subview content size + spacing

  • UIStackView 沿轴方向长度 = 所有排列子视图大小之和 + 子视图之间的间距总和

  • UIStackView 正交轴方向(垂直于轴方向)长度 = 最大排列子视图的长度

  • isLayoutwarginsReLativeArrangement 为 true,上述的长度还会包含相关的 layoutMargins

上面所说的长度都是拟合大小 (fitting size)

注意:

这里的子视图大小是视图的 content size,内容大小,是指 Auto Layout 约束计算之后的 size,所以直接设置 frame是无效的,必须通过重写 intrinsieContentsize 属性或给子视图的宽高添加 Auto Layout 约束

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

  • 让 UIStackView 能自适应子视图大小的前提是子视图要有 content size。

  • 最终子视图的 size 也不一定等于 content size,当 UIStackView 自身设置了宽高约束,其会为了填充空间会对子视图进行拉伸或收缩

  • 自适应子视图大小意味着其不允许子视图溢出其自身。这与 CSS flexbox 的 flex-wrap 表现有别。

另外 UIStackView 的这些布局属性会直接影响其自适应的大小:

  • axis:定义了堆叠的轴方向,是在垂直方向还是水平方向进行堆叠。
  • distribution:定义了轴方向上的子视图布局。
  • alignment:定义了轴正交方向上的子视图布局。
  • spacing:定义了轴方向上的子视图之间的最小间距
  • isBaselineRelativeArrangement:定义了视图之间的垂直间距是否从基线测量
  • isLayoutMarginsRelativeArrangement:定义了是否要基于子视图的 LayoutMargins 来布局

若修改上述属性无法达到你的预期效果,则优先检查 Xcode 控制台是否输出了 Auto Layout 约束冲突的错误日志,从中检查需要修改的属性或补充的约束。

NSLayoutConstraint.Axis

默认为horizontal水平方向

UIStackView.Distribution

定义沿 UIStackView 轴方向的子视图的大小和位置的布局。

除了 fillEqually 以外的 distribution,UStackView 在沿轴方向计算尺寸时,会使用每个子视图的 intrinsicContentsize 属性。而 fiLLEqually 会相等调整子视图的大小,使其在轴方向的长度是一致的,如果可能,UIStackView 会拉伸所有子视图,以匹配轴方向最大内容大小的视图。

  1. fill 默认情况,UIStackView 调整其子视图的大小,以填充轴方向上的可用空间

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

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

    当存在歧义时,UIStackView 根据子视图在 arrangedSubviews 中的索引调整子视图的大小

  2. fillEqually

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

  3. fillPropertionlly

    UIStackView 调整其子视图的大小,以填充轴方向上的可用空间。视图根据其沿 UIStackView 轴的内在内容大小按比例调整大小。

  4. equalSpacing

    UIStackView 放置排列子视图,以填充轴方向上的可用空间。

    当排列的视图没有填充 UIStackView 时:UIstackview 会均匀地填充视图之间行间距。即此时的 spaicing 只限定了最小的间距

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

  5. equalCentering

    对⼦视图中点等距布局,同时保持⼦视图之间的间距。同样,此时的 spaicing 只限定了最⼩的间距。

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

    当存在歧义时,UIStackView 会根据其在 arrangedsubviews 中的索引收缩子视图

    为了保持⼦视图内容⼤⼩,UIStackView 会突破中点等距布局。同样,为保持⼦视图间的最⼩间距, UIStackView 会压缩⼦视图的内容⼤⼩。

UIStackView.Alignment

定义垂直于 UIStackView 轴方向的子视图布局。

对于除 fill 之外的所有 alignmentUIStackView 在计算轴正交方向的大小时使用每个子视图的 intrinsiccontentsize 属性。fill 则调整所有子视图的大小,以填充轴正交方向上的可用空间,如果可能,UIStackView 会拉伸所有子视图,以匹配轴正交方向上最大内在大小的视图。

  1. fill 默认。UIStackView 调整其子视图大小,以填充轴正交方向上的可用空间。

  2. center

    UIStackView 把⼦视图中点沿轴对⻬,即垂直方向居中对⻬。

  3. leading:横轴时也可以用top

    UIStackView把子视图沿着前边缘对齐

  4. trailing:横轴时也可以用bottom

  5. fisrtBaseline

    仅横轴有效,UIStackView根据首个基线对齐排列子视图

  6. lastBaseline

    仅纵轴有效,UIStackView根据末尾基线对齐排列子视图

Spacing间距

  1. 固定间距

    1
    
    var spacing: CGFloat { get set }
    

    默认为0.0,此属性定义了在UIStackViewDistribution.fill子视图之间的严格间距,也是UIStackView.Distribution.equalSpacingUIStackView.Distribution.equalCentering 的最小间距

    使用负值会重叠子视图,其堆叠层级按子视图的层级索引排列

    更进一步,iOS 11.0+ 还增加了设置自定义间距的方法

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

    通过这个方法可以设置子视图之间的间距,如下图所示,只可以设置两个subView之间的间距,而不可以设置一个subView到supperView的边缘的距离

    这个时候可以通过给边缘位置添加一个占位视图的方式来进行相关设置,这里简单封装一个占位视图:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    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)
        }
    }
    

    音乐入口代码如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    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))
    }
    

    效果:

子视图管理

1
2
3
4
func addArrangedSubview(_ view: UIView)
var arrangedSubviews: [UIView] { get }
func insertArrangedSubview(_ view: UIView, at stackIndex: Int)
func removeArrangedSubview(_ view: UIView)
  1. 上述所有方法都会作用于arrangedSubviews数组,调用UIStackView的addArrangedSubview(_:)时,添加的视图除了添加到 arrangedSubviews 中,同时也会添加到基类的 subviews 中,即成为子视图。

  2. 由于 UIStackView 内部会确保 arrangedSubviewssubviews 的子集,所以即使在调用 addArrangedSubview(_:) 前调用了基类的 addSubview(_:),也不会有什么影响,也不会改变其在 arrangedSubviews 中的顺序。但必须要调用 addArrangedSubview(_:) 来添加管理的子视图,否则设置 UIStackView 的各个属性将无法作用于添加的子视图。

  3. 移除视图的时候要注意,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:) 中控制。

0%