UIStackView

UIStackView

UIStackview是一个流式布局的工具。贼好用😝😝

Overview

UIStackView可以说是Auto Layout最强有力的助手,通过它能大幅简化多个view规律性的布局逻辑。stack view所管理的view都在它的 arrangedSubviews 属性里。stack view会把这些view根据其在数组中的先后顺序沿着横向/纵向(取决于axis属性)排列,然当具体的排列效果还和 distribution, alignment, spacing, 等属性有关。

在Storyboard中使用stack view很简单,打开控件库,选择Horizontal Stack View 或 Vertical Stack View拖到view上,再拖一些其他控件到stack view里面就行了。如果约束足够明确的话Interface Builder会根据你的内容自动调整stack view的大小。另外还有一个快速使用stack view的小技巧,就是选中一组控件以后点击Interface Builder右下角的 Embed In 按钮选择stack view就能快速用stack view把选中的控件wrap起来。

Stack View 和 Auto Layout

stack view通过自动布局来确定所管理的view的大小和位置。axis决定了stack view是水平排列还是垂直排列。如果设置 isLayoutMarginsRelativeArrangement 属性为 true ,stack view会基于margin来决定view之间位置(不设置的话就是基于视图的边缘来计算)。

distribution属性用来设置沿着axis方向的各个view的排列方式。当axis是UILayoutConstraintAxisHorizontal就是设置水平方向各个view的排列关系,当axis是UILayoutConstraintAxisVertical就是设置垂直方向各个view的排列关系。

stack view对子view进行排列时,会依次调用view的intrinsicContentSize属性来确定其大小。有一个例外,就是distribution设置成UIStackView.Distribution.fillEqually时,此时stack view会尽量把所有的view拉伸成同样的长度,均匀地填充整个容器。stack view在拉伸时会尝试把其他的view拉得和最长的那个一样长。

alignment属性用来设置垂直于axis方向的各个view的对齐方式。当axis是UILayoutConstraintAxisHorizontal就是设置垂直方向各个view的对齐关系,当axis是UILayoutConstraintAxisVertical就是设置水平方向各个view的对齐关系。

stack view对子view进行对齐时,也会依次调用view的intrinsicContentSize属性来确定其大小。当然也有一个例外,就是alignment设置为UIStackView.Alignment.fill 时,此时stack view会尽量把所有的view拉伸成同样的高度,均匀地填充整个容器。stack view在拉伸时会尝试把其他的view拉得和最高的那个一样高。

Stack View的位置和大小

stack view可以根据所管理的view推导出自己的大小,但是它无法确定自己的位置。所需要明确地写出位置约束。只要指定两个的相邻边的位置stack view就能确定其自身的位置。与此同时stack view也会根据内容来自动调整自己的宽度和高度:

  • 沿着axis方向的宽度等于其管理的所有view的尺寸加上view之间的间距
  • 垂直于axis方向的高度等于所管理的view中最高的那个
  • 如果stack view的 isLayoutMarginsRelativeArrangement 属性设置为 true, stack view的尺寸会包含margins的大小

当然,上面的这些都是默认的行为,如果不能满足要求还能直接给stack view加高度或宽度的约束。stack view会根据 UIStackView.DistributionUIStackView.Alignment 等设置来调整子view的排列和对齐来满足约束条件。

虽然stack view仅仅只是一个布局容器,但是它也支持基于first/last baseline的布局:

  • horizontal方向的stack view forFirstBaselineLayoutforLastBaselineLayout 会返回所管理的view中最高的那个对应的属性。 如果最高的那个也是stack view的话会进行递归调用,直到返回正常结果
  • vertical方向的stack view forFirstBaselineLayout会返回所管理的第一个子view,forLastBaselineLayout会返回最后一个。同样的如果第一个/最后一个还是stack view的话也会递归式调用

注意

Baseline alignment 仅仅只能在view的frame高度等于自己的intrinsic.height时才能正常工作,换句话说就是view如果被压缩或拉伸的话baseline alignment可能就会有bug

Stack View的常用属性

stack view虽然是view的子类,它只负责管理其排列视图的位置和大小,本身不会被渲染出来。所以一些外观相关的属性(如backgroundColor)对它没有效果,也不能重载 layerClass, draw(_:)等方法。

  • axis 确定了stack的方向,选项是vertically/horizontally.
  • distribution 用来设置沿着axis方向的各个view的排列方式
  • alignment 用来设置垂直与axis方向的各个view的对齐方式
  • spacing view之间的最小间距
  • isBaselineRelativeArrangement 当axis设置为垂直方向时才有用。垂直方向的view是否用基于baseline排列,这个属性在排列一些文本控件时很管用
  • isLayoutMarginsRelativeArrangement 是否基于margin进行排列

Stack View的Arranged Views和Subviews

stack view保证 arrangedSubviews 属性一直是其 subviews 的子集. 具体规则如下:

  • 当stack view添加view到arrangedSubviews 时, 它也会把view添加到自己的subviews数组里面(如果已经添加过就不重复添加)

  • 当一个view从stack view的subview中移除时,它也会从 arrangedSubviews 中移除

  • 当一个view从stack view的 arrangedSubviews 中移除时,这个view 并不会从stack view的arrangedSubviews中移除

虽然arrangedSubviews属性一直是其subviews的子集,但这两个数组内的元素顺序并不一定是一致的。

  • arrangedSubviews中的顺序决定了view在容器中出现的顺序。在水平方向的stack view中,view是沿着阅读方向(绝大部分语言是从左到右,阿拉伯语是从右到左。不过这个不取决于系统语言设置,取决于布局是用left/right还是leading/trailing)排列的。index小的在前,index大的在后
  • subviews 中的顺序决定子view的Z-order。Z-order高的会把低的view盖住

Stack View 动态内容

当对stack viewarrangedSubviews 进行添加/插入/删除,以及对它所管理的子view的属性进行修改时,stack view的内容会自动更新。

1
2
3
4
// Appears to remove the first arranged view from the stack.
// The view is still inside the stack, it's just no longer visible, and no longer contributes to the layout.
let firstView = stackView.arrangedSubviews[0]
firstView.isHidden = true

同样的,如果修改stack view的属性,它的内容也会根据属性的变化而动态地调整。比如设置axis动态地改变stack view的方向。

1
2
3
4
5
6
7
// Toggle between a vertical and horizontal stack
if stackView.axis == .Horizontal {
stackView.axis = .Vertical
}
else {
stackView.axis = .Horizontal
}

也可以在动画block里面调整子view的属性。

1
2
3
4
5
// Animates removing the first item in the stack.
UIView.animateWithDuration(0.25) { () -> Void in
let firstView = stackView.arrangedSubviews[0]
firstView.isHidden = true
}