Project6:Auto Layout

概述

摘要:熟悉Auto Layout

概念:Auto Layout

1. 设置

2.进阶Auto Layout

3.代码中的Auto Layout:添加限制

4.Auto Layout规则和优先级:constrainWithVisualFormat

5.总结


设置

在这个技术项目中你会学到更多关于Auto Layout这一iOS让你用来设计布局的富有表现力的东西。我们在项目2中用它来保证国旗按钮在正确的位置,但那个项目有个问题:如果你旋转你的设备,国旗就不对了!

所以,我们会先修复项目2这样它能带来更多关于进阶Auto Layout技术的知识,然后看下如何在代码中使用它的方法。

第一步,复制项目2,重命名为项目6a,然后在Xcode中打开,好了,现在开始吧!


进阶Auto Layout

当你运行这个项目时,竖直方向很OK,但是横屏时就不行了——有个按钮被挡住了。你有两个选项:要么不允许横屏,要么让布局在两个方向都可以使用。

不允许横屏明显不是个好办法,但有时却是正确的。大多数游戏,比如阻止横屏就是因为两个方向都支持没有意义。如果你想要这样做,按下Cmd+1显示项目导航窗格,选择你的项目,然后右边会出现一个带有“PROJECT”和“TARGETS”的窗格,中间还有些信息。

请注意:这个项目和目标列表可以被隐藏,方法是点击项目编辑器顶部左边的关闭按钮(就在一个四个方格的图标正下方),而你可能正好发现你的已经被隐藏了。我强烈建议你显示这个列表——隐藏起来只会让东西更难找,所以,确保你可以看见它!

现在你看到的就是项目编辑器,包含了很多能影响你的app工作的选项。你会在将来经常使用它,所以记住怎么找到它!选择TARGETS下面的Project2,然后选择General tab,接着往下拉,直到你看到设备方向的四个勾选框,你可以选择你想要支持的方向。

在后续的项目中你会需要支持一些精选方向,但现在我们实现最好的解决办法:向Auto Layout中添加额外的规则,这样它就能在横屏方向也能很好显示。

在IB中打开Main.storyboard,选择底部的国旗,然后Ctrl拖拽到它下面视图控制器的空白区域中。拖拽的方向很重要,所以请垂直向下拖拽。

当你松手时,一个带有“Bottom Space to Bottom Layout Guide”选项的弹窗会出现——请勾选。它创建了一个新的Auto Layout规则即国旗的底部必须距离视图控制器的底部X点的距离,X值等于现在国旗底部跟视图控制器的距离,无论多少。

尽管这是个有效的规则,但它还是会固定你的布局,因为我们现在有了一套完整的垂直方向上的规则:顶部的国旗距离视图控制器顶部36点,中间的距离顶部的30点,底部的距离中间的30点,从底部国旗的底部到视图控制器的底部距离为X。这个X对我来说现在是140,你的可能不一样。

因为我们已经告诉Auto Layout整个空间有多大,它会把它们整个都加起来然后再把剩下的在三面国旗中间的距离以它认为最好的方式分配好。这就是,国旗现在都必须在垂直方向上被来填满这个空间,这也是我们最不想要的。

相反的,我们会告诉Auto Layout哪些地方是灵活的,就在刚创建的新的底部规则中。底部的国旗不是非得距离底部140点——距离必须是某个特定的值让它可以不碰到边沿。如果还有更多空间,那很好,Auto Layout会利用起来,但我们关心的是最小值。

选中第三面旗子来看它的蓝色限制条件列表,然后(仔细点!)选中我们刚添加的底部国旗的限制条件。点击右边的属性观察器,你会看到Relation设置为Equal,Constant设置为140(或其他什么值,取决于你自己的布局)。

你需要做的是把Equal改成“Greater Than or Equal”(大于等于),然后把Constant的值改成20。这样规则就变成“距离底部至少是20,但你可以给它更多”。你在做的时候布局不会自动修正,因为结果是一样的。但至少现在Auto Layout知道它在拉升国旗之外还有其他的灵活性可用。

我们的问题还没修复:在横屏时,iPhone4s只有320点的空间来操作,所以Auto Layout会根据布局规则消除一面或者甚至两面。消除国旗不是很好,大小不一的国旗也不太好,所以我们要增加更多的规则。

选中第二个按钮,然后Ctrl拖拽到第一个上。然后在给出的选项列表中,选择Equal Heights。 现在在第三个按钮到第二个按钮上重复同样的动作。这个规则保证三面旗子总是高度相同,所以Auto Layout再也不能通过挤压一个按钮来满足规则,而是要同时均等挤压三个。

这样能修复部分问题,但另外一些角度来说,问题更严重了。现在被压扁的旗子从一面变成了三面!但再加上一条规则,我们就能让旗子不再被压扁。选中第一个按钮,然后Ctrl拖拽到上面一点——但是还在按钮中!当你松掉左键时,你会看到“Aspect Ratio”属性,勾选它。

Aspect Ratio限制为的就是一次性解决所有的挤压问题:它代表的是如果Auto Layout要缩减国旗的高度,它就会相应地缩减国旗的宽度,也就是说,国旗总是跟原来的看起来一样,只是大小不同。给另外两面旗子也添加Aspect Ratio,然后再运行你的app,它就会在两个方向都表现不错,感谢Auto Layout!

原文链接:

https://www.hackingwithswift.com/read/6/2/advanced-auto-layout


代码中的Auto Layout:添加限制

在Xcode中新建一个Single View Application project,命名为Project6b,目标为iPhone。我们要手动创建一些视图,然后用Auto Layout安置它们。把下面的代码放入你的viewDidLoad()方法中:

override func viewDidLoad() {

       super.viewDidLoad()

       let label1 = UILabel()

       label1.translatesAutoresizingMaskIntoConstraints = false

       label1.backgroundColor = UIColor.redColor()

       label1.text = "THESE"

       let label2 = UILabel()

       label2.translatesAutoresizingMaskIntoConstraints = false

       label2.backgroundColor = UIColor.cyanColor()

       label2.text = "ARE"

       let label3 = UILabel()

       label3.translatesAutoresizingMaskIntoConstraints = false

       label3.backgroundColor = UIColor.yellowColor()

       label3.text = "SOME"

       let label4 = UILabel()

       label4.translatesAutoresizingMaskIntoConstraints = false

       label4.backgroundColor = UIColor.greenColor()

       label4.text = "AWESOME"

       let label5 = UILabel()

       label5.translatesAutoresizingMaskIntoConstraints = false

       label5.backgroundColor = UIColor.orangeColor()

       label5.text = "LABELS"

       view.addSubview(label1)

       view.addSubview(label2)

       view.addSubview(label3)

       view.addSubview(label4)

       view.addSubview(label5)

}

先暂时不管这些代码有什么用,请把下面的方法加到viewDidLoad()后面某个位置:

override func prefersStatusBarHidden() -> Bool {

       return true

}

这个方法告诉iOS我们不想在这个视图控制器现实iOS状态栏——这就是告诉你现在几点的数据位。

OK,回到viewDidLoad():代码总共创建了5个UILabel对象,每个都有特定的文本内容和背景色。所有视图随后都被view.addSubview()添加到我们的视图控制器中。同时我们还把标签的translatesAutoresizingMaskIntoConstraints属性设置为false,因为iOS默认根据视图的尺寸和位置来生成Auto Layout限制条件。我们要手动完成,所以就禁止了这一功能。

如果你现在运行app,在顶部你会看到一些重叠着的彩色标签,所以它看上去像是“LABELS ME”。这是因为我们的标签都在它们的默认位置上(顶部居左),大小也是根据内容来的。

我们要做的是添加如下内容的限制条件:每个标签都从父视图的左边沿开始到右边沿结束。我们要用Auto Layout Visual Format Language(VFL)来做这件事,VFL类似于用键盘符号来画出布局的方法。

做之前,我们需要创建一个我们想要布局的视图的字典。这虽然是一种新的数据类型,但很好懂。你已经知道数组,也就是可以按顺序读取值的类型,比如myArray[0]读取的是该数组的第一个元素。字典读取值时用的不是数字,而是你制定的作为获取方法(key)的任何对象,比如你可以通过myDictionary["name"]读取你想要的值。

用VFL的理由很快就会出来了,但你得在addSubview()后面先加上下面的字典:

let viewsDictionary = ["label1": label1, "label2": label2, "label3": label3, "label4": label4, "label5": label5]

这里创建了一个带字符串(key)和UILabel(value)的字典。所以,要得到label1,我们仙子阿可以用viewDictionary["label1"]。看上去可能有点多余,但一会儿你就会知道:轮到VFL了!

在刚创建的字典下面添加以下代码:

view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label1]|", options: [], metrics: nil, views: viewsDictionary))

view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label2]|", options: [], metrics: nil, views: viewsDictionary))

view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label3]|", options: [], metrics: nil, views: viewsDictionary))

view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label4]|", options: [], metrics: nil, views: viewsDictionary))

view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[label5]|", options: [], metrics: nil, views: viewsDictionary))

量很大,但实际上只是同样的事情重复了五遍。所以我们可以改写成循环:

for label in viewDictionary.keys {

      view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[\(label)]|", options: [], metrics: nil, views: viewsDictionary))

}

上面我们用字符串插值把key放入到了VFL。

让我们简化下,然后集中注意力到剩下的内容中:

view.addConstraints():向我们视图控制器的视图添加了一组限制条件。用一组而不是一个限制是因为VFL可以同时创建多个。

NSLayoutConstraint.constraintsWithVisualFormat()是把VFL转换成一组限制条件的Auto Layout方法。它接受很多参数,但最重要的是第一个和最后一个。

我们给选项参数一个空数组[],给规则参数一个nil。你可以用这些选项来定制VFL的含义,但现在我们暂时不管。

这是最简单的部分。所以,让我们看下VFL本身:"H:|[label1]|"。这是个字符串,它描述了我们想要的布局外观。VFL把它转化成Auto Layout限制,然后添加到视图中。

H:代表我们要定义一个水平方向的布局;我们很快还会做个垂直方向的。符号“|”代表视图的边沿。我们要把这些限制添加到视图控制器的主视图中,所以它很高效地表示了“视图控制器的边沿”。最后,我们用[label1]表示“把label1放这里”。想象“[]”表示视图的边沿。

所以,"H: |[label1]|"表示“水平方向上,我希望我的label1从我的视图的左边到右边。”但这里有个小插曲,什么是“label1”?是的,我们知道它是什么因为这就是我们的变量名,但变量名只是对人类有用——程序运行时变量名并不实际被保存和使用。

这就引入了我们的字典viewDictionary:我们把key设置成字符串,value设置成UILabel,然后“label1”就是我们的标签。这个字典是跟着VFL一起的,被iOS用来找从VFL那里得来的名字。所以当它看到[label1],它会在字典中找到key“label1”,然后生成Auto Layout限制。

以下是VFL行的完整解释:视图中我们的每个标签都要挨在一起放置。如果你现在运行程序,就是你看到的那种,但它同时也高亮了我们的第二个问题:我们没有垂直方向的布局,所以虽然所有的label都是肩并肩挨在一起,但他们全都是重叠着的。

我们会用另外一组限制条件来修复,虽然只有一行但很长:

view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[label1]-[label2]-[label3]-[label4]-[label5]", options: [], metrics: nil, views: viewsDictionary))

跟之前的五行一样,除了VFL部分。这次我们指定V:,表示这些限制实在垂直方向上的。我们在VFL中有多个视图,所以会生成大量限制。这次VFL中的新东西是“-”符号,代表“空格”。默认为10点,但你可以自定义。

记住我们的垂直VFL后面没有一竖,所以我们不会让最后一个标签拉伸到视图的边沿。这会在最后最后一个标签的后面留下空白,正好是我们想要的。

如果你现在运行APP,你会看到5个标签水平方向上相互紧挨着,垂直方向排列整齐。如果要换做Ctrl拖拽操作,工作量巨大,所以我希望你可以为VFL的能力点个赞!

原文链接:

https://www.hackingwithswift.com/read/6/3/auto-layout-in-code-addconstraints


Auto Layout规则和优先级:constrainWithVisualFormat

现在我们有了一个可以干活的布局了,但还是非常的基础:标签不是很高,而且没有应对最后一个标签的底部可能被底边挤压的规则。

要修复这个问题,我们会为底边添加一个限制条件,即最后一个标签的底部必须离开视图控制器的底部10点的距离。我们也会告诉Auto Layout,我们希望这5个标签的高都是88点。用下面的代码来替换之前的垂直限制条件:

view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[label1(==88)]-[label2(==88)]-[label3(==88)]-[label4(==88)]-[label5(==88)]-(>=10)-|", options: [], metrics: nil, views: viewsDictionary))

区别在于我们现在在圆括号内有了数字:(==88)是给标签,(>=10)是给底部的空间。注意,当指定空间的大小时,你需要在尺寸的前后使用“-”,比如,-(>=10)-。

我们指定了两种尺寸:==和>=。第一种表示“完全等于”,第二种表示“大于等于”。所以,我们的标签一定会是个准确尺寸,我们确保底部还有些空间的同时也让它更加灵活——它至少会是10点,但会不会是100点甚至更多取决于环境。

实际上,等一等。标签尺寸我不想要88点了,我想要80点。去把所有的标签都改成80点高。

What?!这就像是你刚收到了一封来自你的IT主管的E-mail:他认为80点的标签更苗条;他们得是64点,因为所有的好尺寸都是2的几次方。

现在,看上去你的主观和你的设计师要干一架了。打了一会儿之后,他们决定搁置争议,取一个中间值:72。所以请继续把所有的标签都改成72点高。

已经烦了?你应该会的。这很容易变成一种像素推敲,特别是如果你的app是被一个委员会设计出来的。

Auto Layout有解决办法,它叫做公制(metrics)。所有这些对constraintsWithVisualFormat()的调用都给了metrics参数nil值,但是时候改改了。你看,你可以给VFL一组带有名字的尺寸,然后用VFL里面的这一组尺寸而不是硬编码的数字。比如,我们想要我们的标签高度为88,我们会创建一个metrics字典:

let metrics = ["labelHeight": 88]

然后,之前无论何时我们写过的==88都可以直接写labelHeight。所以,把现在的垂直限制改成下面的内容:

view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[label1(labelHeight)]-[label2(labelHeight)]-[label3(labelHeight)]-[label4(labelHeight)]-[label5(labelHeight)]->=10-|", options: [], metrics: ["labelHeight": 88], views: viewsDictionary))

所以当你的设计师、经理、书呆子决定88点是错的而你想要改成其他的数字,你可以只用改一个地方就完事儿了。

在我们结束之前,我们还要再做一个改动让整体的用户体验更棒,因为现在它还是不太完美。具体的话就是,我们要让所有的标签统一高度,然后给顶部和底部加上一些限制。这在竖直方向还好,但在水平方向你可能没有那么多空间来满足所有的限制。

以我们现在的轮廓,你会在app画面旋转到横屏时看到下面的信息:“无法同时满足所有的限制条件。”这意思是说你的限制只是不工作,不论给多少功屏幕空间都不做,这就是优先权的由来。你可以给任何的限制一个优先级,然后Auto Layout会尽全力让它工作。

限制优先级是一个介于1和1000之前的值,1000表示“必需”,其他就是可要可不要。默认中,你的所有限制优先级都是1000,所以Auto Layout会没办法在我们当前的布局中找到解决办法。但如果我们让高度变得可选——甚至变成优先级999——这也意味着Auto Layout可以找到完成我们的布局的解决办法:缩小标签来适应即可。

重要的是要理解Auto Layout不是直接丢弃它无法满足的规则——它还是在尽全力去满足它们。所以在我们的例子中,如果我们让88点的高度变得可选,Auto Layout可能会让它们变成78或者其他数字。这就是,它还是会尽全力让它们接近88。TL;DR:限制是按优先级从高到低的顺序考虑的,但所有的都会被考虑到。

所以,我们要把标签高度的优先级改成999。但我们也要再改动一下,就是告诉Auto Layout我们想要所有的标签高度一致。这很重要,因为如果他们使用labelHeight时全都是可选高度,Auto Layout可能会用一个高一个低的标签来解决布局问题。

从视图自己的角度来说,它成功地让有些标签达到88高度,所以它可能对自己很满意了,但它让用户界面很不平整。所以我们会让第一个标签使用labelHeight的优先级为999,而其他的标签跟第一个标签高度相同。这是新的VFL代码:

"V:|[label1(labelHeight@999)]-[label2(label1)]-[label3(label1)]-[label4(label1)]-[label5(label1)]->=10-|"

这里的@999给了限制一个优先级,告诉Auto Layout让他们高度相同的方法是把(label1)用在其他标签的尺寸位置上。

好了:你的Auto Layout轮廓已经完成了,现在app可以在横竖屏都安全运行了。

原文链接:

https://www.hackingwithswift.com/read/6/4/auto-layout-metrics-and-priorities-constraintswithvisualformat


总结

世界上有两种iOS开发者:一些人用Auto Layout,另一些蠢货。这是个略陡峭的学习曲线,但这是一种特别令人印象深刻的创建可以自适应各种设备的伟大布局的方式。

大多数人推荐你尽量在IB中完成一些任务,是因为好的理由——你可以随意拖拽各种元素,而且可以直观的看到所有的东西都长什么样,而且它还会提醒你是否存在问题。但,你也看到了,在代码中创建限制相当的简单,这要归功于VFL(Visual Format Language),所以你可能发现你自己混用两种办法来得到最好的结果。

原文链接:

https://www.hackingwithswift.com/read/6/5/wrap-up

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

推荐阅读更多精彩内容