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 会拉伸所有子视图,以匹配轴方向最大内容大小的视图。
-
fill
默认情况,UIStackView 调整其子视图的大小,以填充轴方向上的可用空间当子视图塞不进 UIStackView 时,UIStackView 根据其抗压优先级 (compression resistance priority) 收缩视图
当子视图没有塞满UIStackView时,UIStackView根据其拥抱优先级(hugging priority)拉伸时图
当存在歧义时,UIStackView 根据子视图在
arrangedSubviews
中的索引调整子视图的大小 -
fillEqually
UIStackView 调整其子视图的大小,以填充轴方向上的可用空间。子视图会拉伸调整大小(匹配最大的子视图),以保持轴方向上的大小都相等。
-
fillPropertionlly
UIStackView 调整其子视图的大小,以填充轴方向上的可用空间。视图根据其沿 UIStackView 轴的内在内容大小按比例调整大小。
-
equalSpacing
UIStackView 放置排列子视图,以填充轴方向上的可用空间。
当排列的视图没有填充 UIStackView 时:UIstackview 会均匀地填充视图之间行间距。即此时的 spaicing 只限定了最小的间距。
当子视图塞不进 UIStackView 时,UIStackView 会根据其抗压优先级收缩视图。
-
equalCentering
对⼦视图中点等距布局,同时保持⼦视图之间的间距。同样,此时的 spaicing 只限定了最⼩的间距。
当⼦视图塞不进 UIStackView 时,UIStackView 收缩间距,直到达到 spaicing 值。若⼦视图仍塞不进UIStackView,则会根据其抗压优先级收缩子视图
当存在歧义时,UIStackView 会根据其在
arrangedsubviews
中的索引收缩子视图为了保持⼦视图内容⼤⼩,UIStackView 会突破中点等距布局。同样,为保持⼦视图间的最⼩间距, UIStackView 会压缩⼦视图的内容⼤⼩。
UIStackView.Alignment
定义垂直于 UIStackView 轴方向的子视图布局。
对于除 fill
之外的所有 alignment
,UIStackView
在计算轴正交方向的大小时使用每个子视图的 intrinsiccontentsize
属性。fill
则调整所有子视图的大小,以填充轴正交方向上的可用空间,如果可能,UIStackView
会拉伸所有子视图,以匹配轴正交方向上最大内在大小的视图。
-
fill
默认。UIStackView 调整其子视图大小,以填充轴正交方向上的可用空间。 -
center
UIStackView 把⼦视图中点沿轴对⻬,即垂直方向居中对⻬。
-
leading
:横轴时也可以用top
UIStackView把子视图沿着前边缘对齐
-
trailing
:横轴时也可以用bottom
-
fisrtBaseline
仅横轴有效,UIStackView根据首个基线对齐排列子视图
-
lastBaseline
仅纵轴有效,UIStackView根据末尾基线对齐排列子视图
Spacing间距
-
固定间距
1
var spacing: CGFloat { get set }
默认为
0.0
,此属性定义了在UIStackViewDistribution.fill
子视图之间的严格间距,也是UIStackView.Distribution.equalSpacing
和UIStackView.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)) }
效果:
子视图管理
|
|
-
上述所有方法都会作用于
arrangedSubviews
数组,调用UIStackView的addArrangedSubview(_:)
时,添加的视图除了添加到arrangedSubviews
中,同时也会添加到基类的subviews
中,即成为子视图。 -
由于 UIStackView 内部会确保
arrangedSubviews
是subviews
的子集,所以即使在调用addArrangedSubview(_:)
前调用了基类的addSubview(_:)
,也不会有什么影响,也不会改变其在arrangedSubviews
中的顺序。但必须要调用addArrangedSubview(_:)
来添加管理的子视图,否则设置 UIStackView 的各个属性将无法作用于添加的子视图。 -
移除视图的时候要注意,
removeArrangedSubview(_:)
只是从arrangedSubviews
中移除子视图,即移除的子视图不受 UIStackView 管理,但其还在基类的subviews
中,即还在视图层级中。所以要直接从层级中移除子视图,可直接使用基类的removeFromSuperview()
方法。
布局管理
UIStackView 会动态响应以下操作,并自动更新布局:
- 添加、删除或插入到
arrangedSubviews
。 - 修改 UIStackView 定义的所有属性。
- 修改子视图的
isHidden
属性。其效果跟 UIView 对子视图的效果不一致,当值为true
时 UIStackView 会重新计算布局(跟移除视图效果一致),还甚至默认添加了动画(轴方向收缩效果),而 UIView 对子视图isHidden
为true
时不会有布局更新,更不会有动画。
处理第 1 点管理 arrangedSubviews
的几个方法,第 2、3 点涉及的属性都可以添加动画。另外要控制子视图 isHidden
的时长,可以放到 animate(withDuration:animations:)
中控制。