翻译@Auto Layout Guide(自动布局指南)


Auto Layout Cookbook(自动布局使用手册)

Stack Views(利用堆叠视图布局)

本节通过实际例子展示堆叠视图的使用,界面复杂程度不断上升。堆叠视图能够有效降低界面复杂度,加快搭建速度;通过调整其各项属性,做到精细布局。此外,还可以在此基础上进一步添加约束,实现预期效果;然而布局复杂度也会因此上升。

具体源码详见项目Auto Layout Cookbook

Simple Stack View(简单堆叠视图)

本例中,我们利用堆叠视图垂直布局一个标签(label),一个图像视图(image view)以及一个按钮(button)。

图25

Views and Constraints(搭建布局)

拖拽一个堆叠视图(stack view)到画布上;向其中添加一个标签,一个图像视图和一个按钮。随后按照下图添加约束:

图26

  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
Attributes(设置属性)

打开堆叠视图属性面板,设置如下:

Stack Axis Alignment Distribution Spacing
Stack View Vertical Fill Fill 8

然后打开图像视图属性面板,设置如下:

View Attribute Value
Image View Image an image of flowers(一张鲜花图片)
Image View Mode Aspect Fit

最后,打开图像视图尺寸面板,调整外扩和内缩优先级:

Name Horizontal hugging Vertical hugging Horizontal resistance Vertical resistance
Image View 250 249 750 749
Discussion(分析&讨论)

系统根据堆叠视图的内容计算其尺寸。所以,只需固定其位置即可。

这里我们让堆叠视图填充父视图,四周标准间距。内容视图尺寸经过缩放,以适应堆叠视图:水平方向上,拉伸至堆叠视图宽度;垂直方向上,根据各自的内缩和外扩优先级调整高度。图像视图应该被优先缩放,因此其垂直方向上的外扩和内缩优先级应该小于其他内容视图。

随后将图像视图的内容模式(content mode)设置为等比例缩放适配(Aspect Fit)。顾名思义,图像被等比例缩放以适应视图尺寸。因此,无论视图尺寸如何变化,图像也不会变形。

更多关于固定子视图以填充父视图的信息,详见章节Simple Single View(简单独立视图)以及Adaptive Single View(自适应独立视图)

Nested Stack View(嵌套堆叠视图)

这次我们通过嵌套多个堆叠视图实现一个复杂布局。另外为了达到预期效果,还需进一步添加约束。

图27

首先构建视图结构,再添加约束,如下一章节Views and Constraints(搭建布局)所示。

Views and Constraints(搭建布局)

处理层层嵌套的堆叠视图时,遵循”由内而外”原则。从姓名部分的一行开始:并排摆放一个标签和一个文本框,同时选中,依次点选菜单栏Editor > Embed In > Stack View。最后生成一个水平堆叠视图,可以作为姓名部分的一行使用。

创建三行,同时选中,依次点选菜单栏Editor > Embed In > Stack View。我们获得了一个垂直堆叠视图,可以作为姓名部分使用。不断重复,按照下图搭建界面,添加约束。

图28

  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
Attributes(设置属性)

每个堆叠视图拥有自己的属性,影响内容布局。打开属性面板,设置如下:

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 First Baseline Fill Equally 8
Root Vertical Fill Fill 8

另外,将文本框背景颜色设置为浅灰色(light gray)。便于我们在设备方向改变时,观察其尺寸变化。

View Attribute Value
Text View Background Light Gray Color

外扩和内缩优先级决定视图的拉伸顺序。打开尺寸面板,设置如下:

Name Horizontal 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 Name Text Fields 48 250 749 750
Discussion(分析&讨论)

本布局几乎全部通过堆叠视图完成。可惜,并非全部。例如图片应始终保持原始比例。然而我们无法使用章节Simple Stack View(简单堆叠视图)中的技巧。图片四周间距必须保持不变,设置内容模式为”等比例缩放适配(Aspect Fit)”会引入间隙。幸运的是,这里图片宽高比始终为1:1,所以将视图宽高比约束为1:1即可。

注意

对于IB来说,比例约束(Aspect Ratio)就是视图宽高之间的约束。约束中系数(Multiplier)有多种含义。对于比例约束来说,如果View.Width = View.Height,则系数为1,表示1:1等比例约束。

此外,所有文本框都应等宽。然而,它们散落在各个堆叠视图中,无法统一管理。所以,需要添加等宽约束。

同简单堆叠视图一样,必须调整部分视图的外扩和内缩(CHCR)优先级,以便其尺寸随父视图变化。

垂直方向上,上部堆叠视图(Upper Stack)和按钮堆叠视图(Button Stack)之间的空隙需要由文本视图填充。因此,其内缩优先级最低。

水平方向上,标签保持其固有宽度;文本框填充剩余空间。前者的优先级无需修改,因为IB会自动将其内缩优先级设置为251,高于后者;然而,我们仍需主动降低将文本框的外扩和内缩优先级。(译者:最后一句我也不知道为什么🤔)

图片高度应该同代表姓名的堆叠视图保持一致。然而,由于堆叠视图只”包裹”内容,所以图像视图的垂直外扩优先级必须非常低,才能保证其主动降低,与堆叠视图保持一致(而非堆叠视图增高,与图像视图保持一致)。此外,图像视图的等高约束进一步使问题复杂化,因为这导致水平和垂直约束互相依赖。所以文本框的水平内缩优先级必须非常低,否则图像视图不会主动缩小。对于上述情况,将优先级调至48或更低可以解决问题。

Dynamic Stack View(动态堆叠视图)

面对堆叠视图,如何动态的添加和删除内容?接下来,我们将完成这项挑战。内容的删除和添加都附带动画效果;另外,堆叠视图位于滚动视图中,从而使得过长的列表能够滚动。

图29

注意

本例旨在演示如何动态操作堆叠视图,如何在滚动视图中使用堆叠视图。实际开发中,列表最好通过UITableView实现。一般来说,不建议用堆叠视图替代列表视图。应根据具体需求,审慎区分不同技术的使用场景。

Views and Constraints(搭建布局)

界面初始布局很简单:将一个滚动视图置于画布中,填充整个根视图;向其中添加一个堆叠视图,再向堆叠视图中添加一个按钮。随后按照下图添加约束:

图30

  1. 1. Scroll View.Leading = Superview.LeadingMargin
  2. 2. Scroll View.Trailing = Superview.TrailingMargin
  3. 3. Scroll View.Top = Superview.TopMargin
  4. 4. Bottom Layout Guide.Top = Scroll View.Bottom + 20.0
  5. 5. Stack View.Leading = Scroll View.Leading
  6. 6. Stack View.Trailing = Scroll View.Trailing
  7. 7. Stack View.Top = Scroll View.Top
  8. 8. Stack View.Bottom = Scroll View.Bottom
  9. 9. Stack View.Width = Scroll View.Width
Attributes(设置属性)

打开堆叠视图属性面板,设置如下:

Stack Axis Alignment Distribution Spacing
Stack View Vertical Fill Equal Spacing 0
Code(编写代码)

为了添加和删除内容,我们需要编写一些代码。创建一个自定义视图控制器,使其拥有滚动视图和堆叠视图。

  1. class DynamicStackViewController: UIViewController {
  2. @IBOutlet weak private var scrollView: UIScrollView!
  3. @IBOutlet weak private var stackView: UIStackView!
  4. // Method implementations will go here...
  5. }

重写方法viewDidLoad,调整滚动视图的初始位置:我们需要滚动视图的内容从状态栏下边开始。

  1. override func viewDidLoad() {
  2. super.viewDidLoad()
  3. // setup scrollview
  4. let insets = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
  5. scrollView.contentInset = insets
  6. scrollView.scrollIndicatorInsets = insets
  7. }

然后为按钮编写动作方法,用来添加内容。

  1. // MARK: Action Methods
  2. @IBAction func addEntry(sender: AnyObject) {
  3. let stack = stackView
  4. let index = stack.arrangedSubviews.count - 1
  5. let addView = stack.arrangedSubviews[index]
  6. let scroll = scrollView
  7. let offset = CGPoint(x: scroll.contentOffset.x,
  8. y: scroll.contentOffset.y + addView.frame.size.height)
  9. let newView = createEntry()
  10. newView.hidden = true
  11. stack.insertArrangedSubview(newView, atIndex: index)
  12. UIView.animateWithDuration(0.25) { () -> Void in
  13. newView.hidden = false
  14. scroll.contentOffset = offset
  15. }
  16. }

上述代码首先为滚动视图计算新的偏移量,然后创建内容视图。内容视图被添加至堆叠视图时处于隐藏状态。隐藏视图对堆叠视图布局没有任何影响。最后,在动画block中显示视图,并更新滚动视图偏移量。

删除内容的代码类似;与方法addEntry不同的是,deleteStackView不通过IB与任何控件关联,而是以代码的形式与新创建的内容视图关联。

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

这个方法通过动画block隐藏要删除的视图;待到动画结束,将其移出视图结构,这也意味着从堆叠视图内容中移出。

尽管可以添加任意视图,但这里我们使用一个堆叠视图作为内容视图。它包含两个标签,一个显示日期,另一个显示随机十六进制字符串,还有一个删除按钮。

  1. // MARK: - Private Methods
  2. private func createEntry() -> UIView {
  3. let date = NSDateFormatter.localizedStringFromDate(NSDate(), dateStyle: .ShortStyle, timeStyle: .NoStyle)
  4. let number = "\(randomHexQuad())-\(randomHexQuad())-\(randomHexQuad())-\(randomHexQuad())"
  5. let stack = UIStackView()
  6. stack.axis = .Horizontal
  7. stack.alignment = .FirstBaseline
  8. stack.distribution = .Fill
  9. stack.spacing = 8
  10. let dateLabel = UILabel()
  11. dateLabel.text = date
  12. dateLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody)
  13. let numberLabel = UILabel()
  14. numberLabel.text = number
  15. numberLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline)
  16. let deleteButton = UIButton(type: .RoundedRect)
  17. deleteButton.setTitle("Delete", forState: .Normal)
  18. deleteButton.addTarget(self, action: "deleteStackView:", forControlEvents: .TouchUpInside)
  19. stack.addArrangedSubview(dateLabel)
  20. stack.addArrangedSubview(numberLabel)
  21. stack.addArrangedSubview(deleteButton)
  22. return stack
  23. }
  24. private func randomHexQuad() -> String {
  25. return NSString(format: "%X%X%X%X",
  26. arc4random() % 16,
  27. arc4random() % 16,
  28. arc4random() % 16,
  29. arc4random() % 16
  30. ) as String
  31. }
  32. }
Discussion(分析&讨论)

如你所见,堆叠视图能够根据内容的变化(添加或删除),动态调整布局。但请注意:

  • 隐藏视图不会对堆叠视图布局产生任何影响;
  • 视图加入堆叠视图的同时,也会加入当前视图结构;
  • 视图从堆叠视图中移除,不会自动脱离当前视图结构;反过来,视图从当前视图结构中移除,会自动脱离堆叠视图。
  • iOS视图属性hidden是不可以添加动画效果的。但加入堆叠视图后,就可以了,因为动画效果被堆叠视图实现。所以添加或删除内容时才有了渐隐渐现效果。

另外我们还使用了滚动视图,其内容尺寸通过堆叠视图定义。水平尺寸参照等宽约束;垂直尺寸等同于堆叠视图的适配尺寸(fitting size)。添加新内容,堆叠视图变长,一旦超出屏幕高度,滚动自动开启。

更多信息,详见章节Working with Scroll Views(滚动视图的使用)