下面的内容主要介绍了如何使用 stack view 创建一些复杂的布局。Stack view 是一个很强悍的工具,使
用它设计用户界面会十分便捷。Stack view 的一些属性在很大程度上只能控制它的子视图如何排列。你可以通过增加一些额外的自定义约束,来更加精确地控制子视图的排列方式;然而,那样将会使布局变得十分复杂。

有关于这部分内容的源码,请查看 Auto Layout Cookbook 这个项目。

简单的 Stack View

这里用了一个竖直排版的 stack view 对一个 label、一个 image view 和一个 button 进行了布局。



视图和约束

在 Interface Builder 上,拖拽一个垂直方向排布的 stack view,然后在上面添加 flowers label、image view 和 edit button。然后按照下面的方式设置约束条件。



1
2
3
4
1. Stack View.Leading = Superview.LeadingMargin
2. Stack View.Trailing = Superview.TrailingMargin
3. Stack View.Top = Top Layout Guide.Bottom + Standard
4. Bottom Layout Guide.Top = Stack View.Bottom + Standard

属性

在属性检查器中,设置以下属性:

Stack Axis Alignment Distribution Spacing
Stack View Vertical Fill Fill 8

下一步,给 Image View 设置以下属性:

View Attribute Value
Image View Image (一张花的图片)
Image View Mode Aspect Fit

最后,在 Size 检查器中,设置 Image View 的 content-hugging 和 compression-resistance (CHCR) 权重。

Name Horizontal hugging Vertical Hugging Horizontal resistance Vertical resistance
Image View 250 249 750 749

讨论

你需要通过设置约束,将 stack view 固定在 superview 上,另一方面,你还需要处理 stack view 的内部布局逻辑。

在上图中,stack view 以一个标准边距充满了整个父视图。Stack View 中的子视图通过调整充满整个 stack view 的边缘。水平方向上,每个视图通过拉伸以适应 stack view 的宽度。竖直方向上,view 按照之前设置的 CHCR 权重来进行拉伸。Image View 应该按照预留空间的大小进行适配。因此,在竖直方向上,image view 的 CHCR 权重应该要低于 label 和 button 的默认权重。

最后,设置 image view 的 mode 为 Aspect Fit。这个设置会强制 image 去调整比例以适应 image view 的大小,以防止 image 因为 image view 的改变而比例失调。这样设置可以允许 stack view 随意改变大小,不用担心图片变形。

关于如何通过布局使一个 view 充满 superview 的更多内容,请查看 AttributesAdaptive Single View

嵌套 Stack Views

这部分内容讲述了一个由多重嵌套的 stack view 构建成的一个复杂的布局。但是,在下面这个布局示例中,并不是只用 stack view 就能创建的,还需要一些额外的约束条件,更加精确地控制布局。



添加完视图层级之后,如果添加约束将在下一小节讲解。

视图和约束

当处理一个嵌套 stack view 布局时,比较容易的做法是从里向外布局。例如在下图中,先在 Interface Builder 布局 “姓名” 这一行。将 label 和 text field 并排放在一起,然后选中它们两个,点击 Editor > Embed In > Stack View 菜单项。这将为这一行创建一个水平布局的 stack view。

然后,布局剩下的 “姓名” 相关的两行,选中,然后点击 Editor > Embed In > Stack View 菜单项,将会创建另外两个水平布局的 stack view。类似的,完成如下显示的一个布局。



1
2
3
4
5
6
7
1. Root Stack View.Leading = Superview.LeadingMargin
2. Root Stack View.Trailing = Superview.TrailingMargin
3. Root Stack View.Top = Top Layout Guide.Bottom + 20.0
4. Bottom Layout Guide.Top = Root Stack View.Bottom + 20.0
5. Image View.Height = Image View.Width
6. First Name Text Field.Width = Middle Name Text Field.Width
7. First Name Text Field.Width = Last Name Text Field.Width

属性

每个 stack view 有一系列它们自己的属性,这些属性定义了 stack view 里的内容如何排布。在属性检查器中,你可以看到如下属性:

Stack Axis Alignment Distribution Spacing
First Name Horizontal First Baseline Fill 8
Middle Name Horizontal First Baseline Fill 8
Last Name Horizontal First Baseline Fill 8
Name Rows Vertical Fill Fill 8
Upper Horizontal Fill Fill 8
Button Horizontal Fist Baseline Fill Equally 8
Root Vertical Fill Fill 8

除此之外,在布局中设置 text view 的背景色为亮灰色。这样当布局发生变化时,你可以很容易看到 text view 的具体大小。

View Attribute Value
Text View Background Light Gray Color

最后,CHCR 的权重决定了在填充剩余空间时哪个 view 应该被拉伸。在 Size 检查器中,你可是看到如下每个 view CHCR 的权重:

Name Horizontal Vertical hugging Horizontal resistance Vertical resistance
Image View 250 250 48 48
Text View 250 249 250 250
First,Middle,and Last Name Labels 251 251 750 750
First,Middle,and Last Text Fields 48 250 749 750

讨论

在这部分内容中,多个 stack view 互相作用,共同完成了一个复杂的布局。但是,这些 stack view 并不能独立完成所有的布局效果。例如,在一个 image view 改变大小时,里面的 image 应该保持纵横比不变。不幸的是,在 简单的 Stack View 这部分内容用的的技术并没有用到这里。在这里,image 需要紧贴 image view 头部和底部边缘。如果将 image view 的 mode 设置为 Aspect Fit 可能会导致上下留白。幸运的是,在这里 image 的纵横比永远保持为正方形,所以你可以让 image 完全充满 image view,然后约束 image view 的纵横比为 1:1。

备注

在 Interface Builder 中,一个纵横比的约束是一个 view 的高度和宽度约束的合成。Interface Builder 可以以多种方式去显示多条约束。一般情况下,对于纵横比约束,会以一个比例式的形式展示。例如 一个 View.Width = View.Height 约束代表一个 1:1 的长宽比约束。

除此之外,所有的 text fields 宽度应该相同。不幸的是,他们被分布在不同的 stack view 中,这样就不能通过 stack view 进行处理。因此,你必须对这些 view 添加 equal width 约束。

和其他简单的 stack view 一样,你需要改变其中一些 stack view 的 CHCR 属性。以此来定义当父视图的大小发生改变时,view 应该如何压缩或扩展。

竖直方向上,你想让 text view 充满整个 stack view。因此在进行设置时,text view 的 Vertical Hugging 权重一定要低于其他 view。

水平方向上,Label 的大小一般为固有内容的大小。当 text field 进行填充适配时,默认的 CHCR 权重设置可以使 label 不会受到挤压。虽然 Interface Builder 早已经将 label 的 content hugging 权重设置为 251,使它要高于 text fields;但是,你仍然需要将 text fields 的 CHCR 权重设置的更低。

为了使 image view 可以和 name row stack view 一样高,需要适当对其进行压缩。然而,stack view 为了让内容能显示出来,只会尽可能地扩大空间。这意味着一定要将 image view 的 vertical compression resistance 设置的非常低,这样 image view 才会压缩而不是充满 stack view。除此之外,设置 image view 的纵横比之后,会使布局变得十分复杂,因为设置纵横比之后,水平和竖直方向将会相互作用。这意味着还需要将 text fields 的 horizontal content hugging 权重设置一个较低值,以此避免 image view 被压缩。综合考虑,将权重设置为 48 或者更低最为合适。

动态 Stack View

这部分内容展示了如何在运行时动态插入或删除一条 item。所有的变化以动画形式展示。除此之外,图中的 stack view 加在了一个 scroll view 内部,如果一屏幕展示不开,可以进行滚动。



备注

这部分内容意在介绍如何动态操作 stack view,以及如何在 scroll view 中使用 stack view。在真实的 APP 开发中,这种场景一般使用 UITableView。一般情况下,你不应该使用动态 stack view 来代替 table view。并且,使用这种方式创建的界面不能灵活使用其他的布局技巧。

视图和约束

一开始的界面十分简单。在画布上放置一个 scroll view 并使其充满画布。然后,在 scroll view 中放置一个 stack view,并且在 stack view 中放置一个 button。所有控件放置好之后,设置如下约束:



1
2
3
4
5
6
7
8
9
1. Scroll View.Leading = Superview.LeadingMargin
2. Scroll View.Trailing = Superview.TrailingMargin
3. Scroll View.Top = Superview.TopMargin
4. Bottom Layout Guide.Top = Scroll View.Bottom + 20.0
5. Stack View.Leading = Scroll View.Leading
6. Stack View.Trailing = Scroll View.Trailing
7. Stack View.Top = Scroll View.Top
8. Stack View.Bottom = Scroll View.Bottom
9. Stack View.Width = Scroll View.Width

属性

在 Attributes 检查器中,给 stack view 设置如下属性:

Stack | Axis | Alignment | Distribution | Spacing
Stack View | Vertical | Fill | Equal Spacing | 0

代码

这里会通过使用一些代码进行布局。创建一个自定义的 view controller,然后将 scroll view 和 stack view 以 outlets 的方式引入。

1
2
3
4
5
6
7
8
class DynamicStackViewController: UIViewController {

@IBOutlet weak private var scrollView: UIScrollView!
@IBOutlet weak private var stackView: UIStackView!

// Method implementations will go here...

}

接下来,重写 viewDidLoad 方法,在其中设置 scroll view 的位置。如果你想让 scroll view 的内容开始于 status bar 的下面,按照下面代码设置:

1
2
3
4
5
6
7
8
9
override func viewDidLoad() {
super.viewDidLoad()

// setup scrollview
let insets = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets

}

现在,为 button 添加一个 action。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// MARK: Action Methods

@IBAction func addEntry(sender: AnyObject) {

let stack = stackView
let index = stack.arrangedSubviews.count - 1
let addView = stack.arrangedSubviews[index]

let scroll = scrollView
let offset = CGPoint(x: scroll.contentOffset.x,
y: scroll.contentOffset.y + addView.frame.size.height)

let newView = createEntry()
newView.hidden = true
stack.insertArrangedSubview(newView, atIndex: index)

UIView.animateWithDuration(0.25) { () -> Void in
newView.hidden = false
scroll.contentOffset = offset
}
}

这个方法首先计算了 scroll view 的 offset,然后创建了一个新视图。将新视图设置为隐藏并加入 stack view。被隐藏的视图不会影响 stack view 的显示和布局——所以 stack view 的显示效果保持不变。然后,在一个动画的 block 中,设置 view 的显示并更新 scroll view 的 offset,使 view 以动画形式展示出来。

类似的,添加一个删除视图的方法。但是,与 addEntry 方法不同,这个方法不会直接关联 Interface Builder 上的任何控件。而是在 view 创建时,以编码的方式关联上每个 view。

1
2
3
4
5
6
7
8
9
func deleteStackView(sender: UIButton) {
if let view = sender.superview {
UIView.animateWithDuration(0.25, animations: { () -> Void in
view.hidden = true
}, completion: { (success) -> Void in
view.removeFromSuperview()
})
}
}

这个方法里,在 animation 的 block 中隐藏 view。完成动画之后,将 view 从 view 层级中移除。这样就可以使 view 自动从 stack view 中移除。

添加到 stack view 中的条目可以是任意样式,在这个例子中,每个条目是一个 stack view,这个 stack view 中包含了一个显示日期的 label,一个显示十六进制字符串的 label,一个删除 button。

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
// MARK: - Private Methods
private func createEntry() -> UIView {
let date = NSDateFormatter.localizedStringFromDate(NSDate(), dateStyle: .ShortStyle, timeStyle: .NoStyle)
let number = "\(randomHexQuad())-\(randomHexQuad())-\(randomHexQuad())-\(randomHexQuad())"

let stack = UIStackView()
stack.axis = .Horizontal
stack.alignment = .FirstBaseline
stack.distribution = .Fill
stack.spacing = 8

let dateLabel = UILabel()
dateLabel.text = date
dateLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)

let numberLabel = UILabel()
numberLabel.text = number
numberLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)

let deleteButton = UIButton(type: .RoundedRect)
deleteButton.setTitle("Delete", forState: .Normal)
deleteButton.addTarget(self, action: "deleteStackView:", forControlEvents: .TouchUpInside)

stack.addArrangedSubview(dateLabel)
stack.addArrangedSubview(numberLabel)
stack.addArrangedSubview(deleteButton)

return stack
}

private func randomHexQuad() -> String {
return NSString(format: "%X%X%X%X",
arc4random() % 16,
arc4random() % 16,
arc4random() % 16,
arc4random() % 16
) as String
}
}

讨论

在这个样例中,stack view 可以在 APP 运行时动态添加或删除 view。然后 stack view 可以根据内容变化动态改变布局。最后,这里有一些重要的点值得我们记住:

  • 隐藏的 view 一直存在于 stack view 的子 view 数组中. 然而,它们不会展示,也不会影响布局和其他子 view.
  • 将一个 view 加到 stack view 的子 view 数组中时,这个 view 会自动添加到 view 层级.
  • 将一个 view 从 stack 的子 view 数组中移除时,不会从 view 层级中自动移除;将一个 view 从 view 层级中移除,同样也不会从 stack 的子 view 数组中移除.
  • 在 iOS 系统中,view 的 hidden 属性通常情况下么有动画效果.然而在这里,将 view 放到 stack view 的子 view 里面时会有动画效果,这个效果是 stack view 实现的,而不是 view 通过使用 hidden 属性实现的.

这部分内容同时还简单介绍了如何在 scroll view 中使用自动布局。这里在 stack view 和 scroll view 之间设置了一系列的约束,以此来定义 scroll view 内容区域大小。在水平方向上,设置 stack view 的宽度充满 scroll view。竖直方向上,scroll view 的 content size 由 stack view 的大小来决定。stack view 会随着加入的条目越多而变得越长。相应的,scroll view 滚动区域也会随之增加,以适应 stack view 的内容大小。

关于更多信息,请查看 Working with Scroll Views.