5. Stack Views(利用堆叠视图布局)@Auto Layout Guide(自动布局指南)

翻译@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
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(嵌套堆叠视图)

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

图27

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

Views and Constraints(搭建布局)

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

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

图28
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(动态堆叠视图)

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

图29

注意

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

Views and Constraints(搭建布局)

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

图30
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(滚动视图的使用)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容