翻译@Auto Layout Guide(自动布局指南)
- 原文:Auto Layout Guide
- 作者:Apple
- 更新:Yannmm@Github.com
Auto Layout Cookbook(自动布局使用手册)
Stack Views(利用堆叠视图布局)
本节通过实际例子展示堆叠视图的使用,界面复杂程度不断上升。堆叠视图能够有效降低界面复杂度,加快搭建速度;通过调整其各项属性,做到精细布局。此外,还可以在此基础上进一步添加约束,实现预期效果;然而布局复杂度也会因此上升。
具体源码详见项目Auto Layout Cookbook。
Simple Stack View(简单堆叠视图)
本例中,我们利用堆叠视图垂直布局一个标签(label),一个图像视图(image view)以及一个按钮(button)。
Views and Constraints(搭建布局)
拖拽一个堆叠视图(stack view)到画布上;向其中添加一个标签,一个图像视图和一个按钮。随后按照下图添加约束:
Stack View.Leading = Superview.LeadingMargin
Stack View.Trailing = Superview.TrailingMargin
Stack View.Top = Top Layout Guide.Bottom + Standard
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(嵌套堆叠视图)
这次我们通过嵌套多个堆叠视图实现一个复杂布局。另外为了达到预期效果,还需进一步添加约束。
首先构建视图结构,再添加约束,如下一章节Views and Constraints(搭建布局)所示。
Views and Constraints(搭建布局)
处理层层嵌套的堆叠视图时,遵循"由内而外"原则。从姓名部分的一行开始:并排摆放一个标签和一个文本框,同时选中,依次点选菜单栏Editor > Embed In > Stack View。最后生成一个水平堆叠视图,可以作为姓名部分的一行使用。
创建三行,同时选中,依次点选菜单栏Editor > Embed In > Stack View。我们获得了一个垂直堆叠视图,可以作为姓名部分使用。不断重复,按照下图搭建界面,添加约束。
Root Stack View.Leading = Superview.LeadingMargin
Root Stack View.Trailing = Superview.TrailingMargin
Root Stack View.Top = Top Layout Guide.Bottom + 20.0
Bottom Layout Guide.Top = Root Stack View.Bottom + 20.0
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(动态堆叠视图)
面对堆叠视图,如何动态的添加和删除内容?接下来,我们将完成这项挑战。内容的删除和添加都附带动画效果;另外,堆叠视图位于滚动视图中,从而使得过长的列表能够滚动。
注意
本例旨在演示如何动态操作堆叠视图,如何在滚动视图中使用堆叠视图。实际开发中,列表最好通过UITableView实现。一般来说,不建议用堆叠视图替代列表视图。应根据具体需求,审慎区分不同技术的使用场景。
Views and Constraints(搭建布局)
界面初始布局很简单:将一个滚动视图置于画布中,填充整个根视图;向其中添加一个堆叠视图,再向堆叠视图中添加一个按钮。随后按照下图添加约束:
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 | Axis | Alignment | Distribution | Spacing |
---|---|---|---|---|
Stack View | Vertical | Fill | Equal Spacing | 0 |
Code(编写代码)
为了添加和删除内容,我们需要编写一些代码。创建一个自定义视图控制器,使其拥有滚动视图和堆叠视图。
class DynamicStackViewController: UIViewController {
@IBOutlet weak private var scrollView: UIScrollView!
@IBOutlet weak private var stackView: UIStackView!
// Method implementations will go here...
}
重写方法viewDidLoad,调整滚动视图的初始位置:我们需要滚动视图的内容从状态栏下边开始。
override func viewDidLoad() {
super.viewDidLoad()
// setup scrollview
let insets = UIEdgeInsetsMake(20.0, 0.0, 0.0, 0.0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
}
然后为按钮编写动作方法,用来添加内容。
// 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
}
}
上述代码首先为滚动视图计算新的偏移量,然后创建内容视图。内容视图被添加至堆叠视图时处于隐藏状态。隐藏视图对堆叠视图布局没有任何影响。最后,在动画block中显示视图,并更新滚动视图偏移量。
删除内容的代码类似;与方法addEntry
不同的是,deleteStackView
不通过IB与任何控件关联,而是以代码的形式与新创建的内容视图关联。
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()
})
}
}
这个方法通过动画block隐藏要删除的视图;待到动画结束,将其移出视图结构,这也意味着从堆叠视图内容中移出。
尽管可以添加任意视图,但这里我们使用一个堆叠视图作为内容视图。它包含两个标签,一个显示日期,另一个显示随机十六进制字符串,还有一个删除按钮。
// 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
}
}
Discussion(分析&讨论)
如你所见,堆叠视图能够根据内容的变化(添加或删除),动态调整布局。但请注意:
- 隐藏视图不会对堆叠视图布局产生任何影响;
- 视图加入堆叠视图的同时,也会加入当前视图结构;
- 视图从堆叠视图中移除,不会自动脱离当前视图结构;反过来,视图从当前视图结构中移除,会自动脱离堆叠视图。
- iOS视图属性hidden是不可以添加动画效果的。但加入堆叠视图后,就可以了,因为动画效果被堆叠视图实现。所以添加或删除内容时才有了渐隐渐现效果。
另外我们还使用了滚动视图,其内容尺寸通过堆叠视图定义。水平尺寸参照等宽约束;垂直尺寸等同于堆叠视图的适配尺寸(fitting size)。添加新内容,堆叠视图变长,一旦超出屏幕高度,滚动自动开启。
更多信息,详见章节Working with Scroll Views(滚动视图的使用)。