之前在看MIT那個(gè)教學(xué)視頻時(shí),對iOS的界面布局點(diǎn)到即止,一直對Auto Layout的原理不太明了。最近重新看了遍官方的文檔,終于對Auto Layout明白了一二。本文對iOS8加入的Size Class以及iOS9加入的Stack Views暫時(shí)不做過多討論,后續(xù)有時(shí)間再補(bǔ)上,我是剛開始學(xué)習(xí)iOS開發(fā),難免有理解錯(cuò)誤的地方,請大家指正。
1 UIView的層次結(jié)構(gòu)
在討論Auto Layout前先來了解下UIView的層次結(jié)構(gòu),在iOS的視圖中,最底層的是UIWindow(UIWindow當(dāng)然也是從UIView繼承而來),其上再是我們的View Controller的UIView,再上面則是我們自己拖拽的各種控件的UIView。要看到UIView的層次結(jié)構(gòu),可以通過Xcode的Debug View HieraHierarchy按鈕來查看。

下面是我創(chuàng)建的一個(gè)測試的工程代碼,選擇的是Single View Application,工程創(chuàng)建好后,Xcode就已經(jīng)為我們創(chuàng)建了一個(gè)View Controller(本文后面用VC來指代View Controller),并設(shè)置好了VC對應(yīng)的Class。我在Main.storyboard的VC對應(yīng)的View上面加入了一個(gè)Button和一個(gè)Label。
我們可以看到這個(gè)測試應(yīng)用的UIView層次結(jié)構(gòu)如下,一共四層:其中最底層為UIWindow,一個(gè)應(yīng)用通常只有一個(gè)UIWindow,它是所有子視圖的根視圖。之上是VC對應(yīng)的UIView,再上一層就是UILabel和UIButton,最上面那層是UIButtonLabel(也就是我們通常見到的 button.titleLabel)。

這些UIView的層次關(guān)系是:
UIWindow.superview -> null
UIView.superview -> UIWindow
UIButton.superview -> UIView
UILabel.superview -> UIView
UIButtonLabel.superview -> UIButton
2 Frame-based Layout
在談?wù)揂uto Layout之前,先看看Auto Layout出現(xiàn)前iOS是通過什么來實(shí)現(xiàn)視圖的布局的。在Auto Layout出現(xiàn)前,iOS開發(fā)要布局視圖是基于frame的,如在我的筆記1中提到的那樣,即只要指定視圖的起始坐標(biāo)(origin)以及寬度(width)和高度(height)即可確定視圖在superview中的位置。如下圖所示,第一個(gè)視圖起始坐標(biāo)為(20,20),寬度是120,高度為80;第二次視圖起始坐標(biāo)為(20,108),寬度高度與第一個(gè)視圖相同:

如果在程序運(yùn)行過程中,如果有視圖的位置改變,則需要重新計(jì)算所有受影響的視圖的位置。通過編碼來實(shí)現(xiàn)位置定位固然有很大的靈活性,單頁帶來了很大的不便,比如我們屏幕尺寸發(fā)生變化,或者旋轉(zhuǎn)屏幕,為了保持之前的布局,就需要修改其中一些視圖的起始位置以及寬度高度等。雖然在UIView中有一個(gè)autoresizingMask的屬性,它對應(yīng)的是一個(gè)枚舉的值,這個(gè)屬性能夠自動(dòng)調(diào)整子控件與父控件中間的位置,寬高等,能夠在一定程度上減輕基于frame布局帶來的不便,但是autoresizingMask并只支持父子視圖之間進(jìn)行約束,并不支持同級(jí)視圖和跨級(jí)視圖的布局。對于復(fù)雜的用戶界面同樣需要編碼進(jìn)行控制。正是由于這些問題,才誕生了我們這篇文章中要討論的Auto Layout。
3 Auto Layout
3.1 Auto Layout基本原理
Auto Layout是一種全新的布局方式,它采用一系列約束(constraints)來實(shí)現(xiàn)自動(dòng)布局,當(dāng)你的屏幕尺寸發(fā)生變化或者屏幕發(fā)生旋轉(zhuǎn)時(shí),可以不用添加代碼來保持原有布局不變,實(shí)現(xiàn)視圖的自動(dòng)布局。
所謂約束,通常是定義了兩個(gè)視圖之間的關(guān)系(當(dāng)然你也可以一個(gè)視圖自己跟自己設(shè)定約束)。如下圖就是一個(gè)約束的例子,當(dāng)然要確定一個(gè)視圖的位置,跟基于frame一樣,也是需要確定視圖的橫縱坐標(biāo)以及寬度和高度的,只是,這個(gè)橫縱坐標(biāo)和寬度高度不再是寫死的數(shù)值,而是根據(jù)約束計(jì)算得來,從而達(dá)到自動(dòng)布局的效果。

約束其實(shí)是一個(gè)兩個(gè)視圖之間的線性關(guān)系。如圖3.1所示,就是Blue View和Red View的一條約束。表示Red View的左邊緣等于Blue View的右邊緣(在從左到右書寫的系統(tǒng)里面,leading=left,trailing=right) + 8個(gè)Point,注意,在iOS代碼里面都是用的邏輯點(diǎn),不是真正的物理像素點(diǎn)。其中關(guān)系可以是=、>=以及<=這三個(gè)的一種,當(dāng)然我們的例子用的是=。
還有一個(gè)要注意的是,這里只是給出了一個(gè)約束來說明約束的基本范式,顯然一個(gè)約束是不能完成Blue View和Red View的自動(dòng)布局的,下一節(jié)通過實(shí)例來看看自動(dòng)布局具體應(yīng)該怎么操作。
3.2 Auto Layout初體驗(yàn) & Fitting Size
新建一個(gè)Single View Application,然后添加一個(gè)View到視圖中,
我們什么約束都不加,發(fā)現(xiàn)Xcode是沒有任何錯(cuò)誤和警告的。但是如果我們自己手動(dòng)加了一條約束(見圖3.2),Xcode卻會(huì)有警告。一開始學(xué)習(xí)都會(huì)有這個(gè)困惑,為什么會(huì)出現(xiàn)這個(gè)情況呢?

原因其實(shí)就是,如果我們什么約束都不加,那么Xcode其實(shí)已經(jīng)幫你自動(dòng)加了約束信息了,這個(gè)約束稱之為prototyping constraints,也就是說,這個(gè)添加的Green View的橫縱坐標(biāo),寬度高度都已經(jīng)設(shè)定為一個(gè)值了(這個(gè)值可以在屬性標(biāo)簽里面看到),所以,Green View的位置已經(jīng)固定,自然Xcode也就不會(huì)有錯(cuò)誤或警告了。而如果我們手動(dòng)加了一條約束,那么Xcode認(rèn)為你要自己添加約束了,那么在Auto Layout引擎檢查約束完備性的時(shí)候自動(dòng)添加的約束會(huì)被忽略,所以,這個(gè)時(shí)候因?yàn)槲覀冎患恿艘粋€(gè)Y軸的約束條件,缺少X軸的約束條件,因此會(huì)報(bào)約束錯(cuò)誤的提示(當(dāng)然這個(gè)并不影響工程的運(yùn)行,你要編譯運(yùn)行還是可以的,而且自動(dòng)添加的約束如果沒有被顯示添加的約束覆蓋,也還是會(huì)生效的,只是控件的位置可能會(huì)存在歧義,影響最終布局效果)。那么我們再加上其他的三個(gè)約束,好了,錯(cuò)誤沒有了。最終添加的約束如下(約束還有優(yōu)先級(jí)這個(gè)非常重要的屬性,后面再談):

這四個(gè)約束可以用下面的四個(gè)等式來表示:
Green View.Trailing = Superview.Trailing Margin
Green View.Leading = Superview.Leading Margin
Green View.Bottom = Bottom Layout Guide.Top + 20
Green View.Top = Top Layout Guide.Bottom + 20
注意到這里引入了幾個(gè)變量,一個(gè)是Top/Bottom Layout Guide(頂部/底部導(dǎo)航),一個(gè)是Superview.leading/Trailing Margin(左/右邊緣間距)。Top Layout Guide其實(shí)是指的根視圖的頂部,模擬器在豎屏下有狀態(tài)欄,狀態(tài)欄默認(rèn)高度為20(注:導(dǎo)航欄與狀態(tài)欄高度不同,導(dǎo)航欄的豎屏默認(rèn)高度為44,橫屏默認(rèn)高度為32),則Green View的Y坐標(biāo)就是20 + 20 = 40。模擬器在橫屏下沒有狀態(tài)欄,則Top Layout Guide.Bottom為0,則Green View的Y坐標(biāo)就是20。Superview.leading Margin在豎屏?xí)r為16,橫屏是為20。這幾個(gè)結(jié)論可以通過打印Green View的frame值來驗(yàn)證:
green view frame:{{16, 40}, {343, 607}} //iPhone6 豎屏
green view frame:{{20, 20}, {627, 335}} //iPhone6 橫屏
我們可以發(fā)現(xiàn),Green View在橫屏和豎屏的大小和位置都是不同的,但是整體布局是我們所希望的效果。這就是Auto Layout做的事情,通過這些約束,根據(jù)屏幕大小不同,屏幕方向不同來動(dòng)態(tài)計(jì)算控件的大小和位置。計(jì)算方法也很簡單,比如我們的例子,因?yàn)閕Phone6的邏輯像素點(diǎn)是375 X 667,因此可以通過上面的約束計(jì)算Green View的大小。由于我們并沒有設(shè)置視圖的大小,視圖最終呈現(xiàn)的大小是由Auto Layout引擎根據(jù)約束計(jì)算得到的,這個(gè)大小也稱之為視圖的Fitting Size,這也就是Auto Layout的便捷之處,我們不需要寫任何代碼去控制。
width = 375 - 16*2 = 343, height = 667 - 40 - 20 = 607 //iPhone6 豎屏
width = 667 - 20*2 = 627, height = 375 - 20*2 = 335 //iPhone6 橫屏
3.3 自身內(nèi)容尺寸 & 抗壓縮抗拉伸效果
先簡化一下這兩個(gè)概念:
- 自身內(nèi)容尺寸(Intrinsic Content Size,以下簡稱ICS)。
- 抗壓縮抗拉伸(Compression-Resistance and Content-Hugging,以下簡稱CRCH)
自身內(nèi)容尺寸
前面我們添加了一個(gè)View到根視圖中,也初次體會(huì)到了Auto Layout的強(qiáng)大之處,接下來我們來添加一個(gè)按鈕。如下圖所示,我們只添加了兩個(gè)約束,Xcode居然沒有報(bào)錯(cuò),這可能讓人納悶了,我們并沒有指定按鈕的寬度和高度,那最終按鈕是如何定位的呢?這就是這一節(jié)要討論的內(nèi)容,一些iOS控件如按鈕控件,文本控件等其實(shí)是有一個(gè)自身內(nèi)容尺寸的,這類控件會(huì)根據(jù)自身內(nèi)容尺寸添加布局約束,如果我們沒有顯示指定控件的寬度和高度,則其自動(dòng)添加的約束就會(huì)起作用。正如下圖中的按鈕,我們只指定了橫縱坐標(biāo)的約束,并沒有指定寬度和高度,但是Xcode并沒有報(bào)錯(cuò)或者警告。

下表列出了一些常用控件的ICS,由表中可以發(fā)現(xiàn),label, button, text fields等都是有ICS的,而UIView和NSView是沒有ICS的。
| View | Intrinsic content size |
|---|---|
| UIView and NSView | No intrinsic content size. |
| Sliders | Defines only the width (iOS). |
| Labels, buttons, switches, and text fields | Defines both the height and the width. |
| Text views and image views | Intrinsic content size can vary. |
控件的ICS基于視圖的當(dāng)前內(nèi)容。Button或者Label的ICS基于其展示的文字?jǐn)?shù)目和字體大小,空的Image View是沒有ICS的,只有當(dāng)你添加了圖片到Image View中,這個(gè)時(shí)候才會(huì)有ICS,而且尺寸大小為圖片的尺寸。
Updated:視圖UIView也是沒有ICS的,有時(shí)候想只指定位置而不指定UIView的大小,可以在Storyboard的Size inspector中設(shè)置Intrinsic Size為Placeholder,這樣便不會(huì)報(bào)錯(cuò)了。注意一點(diǎn)的是,這個(gè)設(shè)置并不影響運(yùn)行時(shí)UIView的Intrinsic Size。
抗壓縮和抗拉伸效果
抗壓縮(Compression-Resistance) 和抗拉伸(Content-Hugging)效果是跟自身內(nèi)容尺寸關(guān)聯(lián)在一起的,如圖3.4所示,抗壓縮定義了視圖抗壓縮的優(yōu)先級(jí),優(yōu)先級(jí)越大,表示越難壓縮;抗拉伸則定義了視圖抗拉伸的優(yōu)先級(jí),優(yōu)先級(jí)越大,則越難被拉伸??箟嚎s和抗拉伸的優(yōu)先級(jí)是針對橫豎兩個(gè)方向的,每個(gè)方向都有一個(gè)優(yōu)先級(jí)。默認(rèn)的View和Button的抗壓縮優(yōu)先級(jí)為750,抗拉伸優(yōu)先級(jí)為250。從優(yōu)先級(jí)大小可以看出來,拉伸一個(gè)View比壓縮一個(gè)View容易。這也符合我們的期望,比如我們期望拉伸一個(gè)按鈕大于其自身內(nèi)容尺寸,而不是縮小按鈕尺寸導(dǎo)致內(nèi)容顯示不全。

// Compression Resistance
View.height >= 0.0 * NotAnAttribute + IntrinsicHeight
View.width >= 0.0 * NotAnAttribute + IntrinsicWidth
// Content Hugging
View.height <= 0.0 * NotAnAttribute + IntrinsicHeight
View.width <= 0.0 * NotAnAttribute + IntrinsicWidth
對于兩個(gè)控件來說,為了滿足Auto Layout的約束,通常會(huì)優(yōu)先壓縮那個(gè)抗壓縮優(yōu)先級(jí)小的控件來適應(yīng)視圖的布局。
下面看一個(gè)例子,我們在視圖中添加一個(gè)Label和一個(gè)Text Field。然后分別設(shè)置了Label的左上的約束和Text Field的右上約束,然后設(shè)置Label和Text Field的間距為20。約束關(guān)系我們可以看到左邊的5個(gè)等式,因?yàn)長abel和Text Field都有自身內(nèi)容尺寸,所以這5個(gè)等式已經(jīng)可以完成布局了。在這個(gè)例子中我們看到Text Field被拉伸了,而Label還是保持自身內(nèi)容尺寸的,這是因?yàn)長abel的默認(rèn)抗拉伸優(yōu)先級(jí)為251大于Text Field的默認(rèn)抗拉伸優(yōu)先級(jí)250,因此Label更難被拉伸,所以看到的是Text Field被拉伸了。那如果我們把Text Field的抗拉伸優(yōu)先級(jí)改為252,則最終運(yùn)行的界面如圖3.5.4所示。




接下來再看一個(gè)Image View的例子,可以看看自身內(nèi)容尺寸和CRCH對Image View的影響。這里我在Image View里面加了個(gè)apple.jpg的圖片,圖片原始尺寸為241*300。開始的時(shí)候我設(shè)置Image View水平垂直居中,不設(shè)置寬度高度,則Image View的寬度和高度為圖片原始尺寸241和300。然后再添加一個(gè)寬度約束,設(shè)置圖片寬度為300。由于顯示添加的約束的默認(rèn)優(yōu)先級(jí)為1000,而Image View的抗拉伸的優(yōu)先級(jí)為251,所以會(huì)以顯示添加的約束為準(zhǔn),圖片寬度會(huì)被拉升到300。而如果我們把顯示添加的寬度約束的優(yōu)先級(jí)改成250,則圖片寬度會(huì)被設(shè)置為原始寬度241。

4 更多例子
4.1 兩個(gè)寬度相等的View

約束關(guān)系:
1.Yellow View.Leading = Superview.LeadingMargin
2.Green View.Leading = Yellow View.Trailing + Standard
3.Green View.Trailing = Superview.TrailingMargin
4.Yellow View.Top = Top Layout Guide.Bottom + 20.0
5.Green View.Top = Top Layout Guide.Bottom + 20.0
6.Bottom Layout Guide.Top = Yellow View.Bottom + 20.0
7.Bottom Layout Guide.Top = Green View.Bottom + 20.0
8.Yellow View.Width = Green View.Width
4.2 兩個(gè)寬度不等的View

約束關(guān)系:
1.Purple View.Leading = Superview.LeadingMargin
2.Orange View.Leading = Purple View.Trailing + Standard
3.Orange View.Trailing = Superview.TrailingMargin
4.Purple View.Top = Top Layout Guide.Bottom + 20.0
5.Orange View.Top = Top Layout Guide.Bottom + 20.0
6.Bottom Layout Guide.Top = Purple View.Bottom + 20.0
7.Bottom Layout Guide.Top = Orange View.Bottom + 20.0
8.Orange View.Width = 2.0 x Purple View.Width
4.3 自身內(nèi)容尺寸

約束:
1.Name Label.Leading = Superview.LeadingMargin
2.Name Text Field.Trailing = Superview.TrailingMargin
3.Name Text Field.Leading = Name Label.Trailing + Standard
4.Name Text Field.Top = Top Layout Guide.Bottom + 20.0
5.Name label.Baseline = Name Text Field.Baseline
這個(gè)例子跟前面提到的類似,注意并不需要設(shè)置Label和Text Field的寬度和高度。而且默認(rèn)設(shè)置中,Label的抗拉伸的優(yōu)先級(jí)251比Text Field的250更高,所以最終看到的效果是Text Field被拉伸了。
4.4 自適應(yīng)View

約束:
1.Blue View.Leading = Superview.LeadingMargin
2.Blue View.Trailing = Superview.TrailingMargin
3.Blue View.Top = Top Layout Guide.Bottom + Standard (Priority 750)
4.Blue View.Top >= Superview.Top + 20.0
5.Bottom Layout Guide.Top = Blue View.Bottom + Standard (Priority 750)
6.Superview.Bottom >= Blue View.Bottom + 20.0
前面的例子都是=的約束,這個(gè)例子加了>=的約束。
注意到我們設(shè)置的>=的約束4優(yōu)先級(jí)比約束3要高,約束6的優(yōu)先級(jí)比約束5的高,這樣如果顯示狀態(tài)欄(模擬器里面豎屏的時(shí)候),我們知道狀態(tài)欄的高度為20,那么這時(shí)約束3滿足的時(shí)候,也就是Blue View的y坐標(biāo)為28(狀態(tài)欄高度20+標(biāo)準(zhǔn)距離8),這時(shí)約束4也滿足,因此會(huì)選擇約束3這個(gè)優(yōu)先級(jí)較低的約束。如果不顯示狀態(tài)欄(模擬器里面橫屏的時(shí)候),則此時(shí)只能滿足約束4,無法滿足約束3。不過Auto Layout引擎會(huì)選擇一個(gè)最接近的約束,也就是設(shè)置Blue View的y坐標(biāo)為20。
更多例子:
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSimpleConstraints.html#//apple_ref/doc/uid/TP40010853-CH12-SW1
https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/ViewswithIntrinsicContentSize.html#//apple_ref/doc/uid/TP40010853-CH13-SW1
Size Class例子:
https://www.raywenderlich.com/113768/adaptive-layout-tutorial-in-ios-9-getting-started
使用代碼和VFL來添加約束可以參見:
http://blog.csdn.net/pucker/article/details/45070955
http://blog.csdn.net/pucker/article/details/45093483