官方文檔:Auto Layout Guide 加上去年WWDC上的 Mysteries of Auto Layout 這兩個 Session,以及星光社的戴銘的這篇總結(jié)深入剖析 Auto Layout,分析 iOS 各版本新增特性可以當(dāng)做小抄使用,涵蓋了 Auto Layout 的所有方面。再寫東西只能寫點(diǎn)不同的了,本文將搜集一些使用 Auto Layout 的痛點(diǎn)和技巧供參考。
Auto Layout 與 Frame
很多人盛贊 Auto Layout 是比 frame 更優(yōu)雅的布局方案,我基本認(rèn)同,不過,首先,Auto Layout 寫起來一點(diǎn)都不優(yōu)雅,一行 frame 代碼使用 Auto Layout 需要四行代碼,足以讓很多人望而卻步。所以官方不斷在提升添加約束的體驗(yàn),第三方庫也不斷地優(yōu)化添加約束的語法來減輕開發(fā)者的痛苦。iOS 9 新推出的 Anchor 在語法上大幅簡化了約束的編寫,不足之處在于缺乏對multiplier參數(shù)的設(shè)置,但不管如何優(yōu)化,frame 一行代碼 Auto Layout 還是四行代碼。就我個人來講,非常不喜歡 VFL,代碼最啰嗦,寫起來簡直要命,不過在 Debug 要讀懂 Log 就必須了解 VFL 的語法。老實(shí)說,使用 frame 時一行代碼四個數(shù)字就可以確定視圖的位置和大小比起有多種可能方案的 Auto Layout 布局實(shí)際上要舒心得多,雖然后者可讀性更好,不過 frame 往往也是需要計算的,從成本上講有時候兩者挺接近的;Auto Layout 的真正優(yōu)勢在于自動化,我們只需要給系統(tǒng)一堆布局方程式,剩下的事情就不用我們操心了,這在分辨率適配、多視圖互相約束協(xié)作等方面顯得極其高效(優(yōu)雅),這也是我能忍受寫一堆約束的原因。
剛接觸 Auto Layout 時一直想搞清楚這兩者的關(guān)系,簡單來講,Auto Layout 將約束條件轉(zhuǎn)化為視圖的 frame。原來的布局過程直接使用 frame 指定視圖的位置和大小,AutoLayout 參與布局后通過約束計算出 frame,再應(yīng)用到視圖上。具體過程可參考 Mysteries of Auto Layout, Part 2 的開頭部分,深入剖析Auto Layout,分析iOS各版本新增特性也總結(jié)了視頻中的這部分內(nèi)容。
如何妥善處理這兩者的關(guān)系?
Frame 自動轉(zhuǎn)化為約束
既想享受 frame 的便捷,又想得到 Auto Layout 的好處,魚與熊掌能兼得么?
UIView 的translatesAutoresizingMaskIntoConstraints屬性在這里派上用場,該屬性為true時,設(shè)置 frame 會自動轉(zhuǎn)化為約束,修改 frame 時也會自動調(diào)整約束。這時候就不要再手動添加約束了,你再添加約束往往會造成沖突,注意是往往,因?yàn)榇藭r視圖上的約束已經(jīng)是唯一可解的了,你添加的往往是優(yōu)先級最高的約束,必然造成沖突,在控制臺能看到NSAutoresizingMaskLayoutConstraint這種類型的約束與你添加的約束無法同時滿足,這里甚至有溫馨提示你查看translatesAutoresizingMaskIntoConstraints屬性的文檔,想必蘋果也知道大家經(jīng)常忘記把這個屬性關(guān)閉?;蛟S你可以添加可選約束,不過這樣一來就沒什么意義了。那么可以修改這種自動添加的NSAutoresizingMaskLayoutConstraint約束嗎?實(shí)際上無法找到這樣的約束的,它被系統(tǒng)隱藏了,你只能在發(fā)生沖突時才能在控制臺看見它們。
這個屬性與原來的 auto resize mask 結(jié)合后能產(chǎn)生很好的效果,如下所示:添加了寬度和高度方向的 mask 后,當(dāng) containerView 的尺寸發(fā)生變化后,subView 也會隨之變化,享受了 Auto Layout 的好處,還不用寫約束。
subView.translatesAutoresizingMaskIntoConstraints = true
subView.autoresizingMask = [UIViewAutoresizing.FlexibleWidth, UIViewAutoresizing.FlexibleHeight]
containerView.addSubview(subView)
subView.frame = containerView.bounds
translatesAutoresizingMaskIntoConstraints屬性將兩種的布局機(jī)制的優(yōu)點(diǎn)結(jié)合起來,你可以在某個子視圖上使用 frame,其他的子視圖使用約束來布局,互不干擾。這種混合機(jī)制就好比 Objective-C 與 Swift 在同一個工程中使用,但你不能在 Objective-C 文件中使用 Swift 語言,或者在 Swift 文件中使用 Objective-C 語言,起初我還真就這么認(rèn)為的。
在 storyboard 里,這個屬性是默認(rèn)關(guān)閉的,在代碼里生成的視圖的該屬性默認(rèn)是開啟的。如果你不想用約束,又希望 AutoLayout 能幫你打理,就開啟這個屬性。其他情況下,應(yīng)該關(guān)閉這個屬性。
各自為政
如果嫌棄寫約束太麻煩,也可以不使用 Auto Layout,使用 Auto Layout 的視圖去折騰約束,剩下的視圖就由你負(fù)責(zé)手動處理 frame 了。覺得 iOS 7 看起來好丑,我還是用我的 iOS 6 吧,沒問題,只是很多新特性無法享受罷了。
直接的混血模式,危險!
正常情況下,兩者的合作方式應(yīng)該只有上面兩種,但經(jīng)常還是有人將 Auto Layout 與 frame 在一個視圖上混合使用,因?yàn)榭瓷先ズ孟穸寄苷9ぷ?,但可能會遇到各種疑難雜癥,原因在于兩者更新布局的機(jī)制差異。
Auto Layout 對視圖進(jìn)行布局的唯一依據(jù)是視圖的約束,約束發(fā)生變化后會觸發(fā)約束機(jī)制重新計算視圖的 frame 并更新,這種情況包括:約束的修改和優(yōu)先級的變化,添加或移除約束,添加或移除添加了約束的視圖。當(dāng)然還有其他事件會觸發(fā)約束變化,文檔 Understanding Auto Layout 中列舉了這些情況。在代碼中我們造成這樣的約束變化后 Auto Layout 會自動更新視圖的布局,但有時候你不能期待布局會立即更新,因?yàn)?Auto Layout 要搜集約束變化,計算新的布局,然后遍歷受影響的視圖重新布局,在性能上比直接設(shè)置 frame 要慢一點(diǎn)。你可以在擁有變化的約束的視圖上調(diào)用layoutIfNeeded()強(qiáng)制立刻更新布局。
直接設(shè)置 frame 并不會修改視圖相關(guān)的約束(除了開啟translatesAutoresizingMaskIntoConstraints),而約束的一切變化會轉(zhuǎn)化為新的 frame,兩者之間的影響是單方向的。同時修改視圖的 frame 和約束,最終結(jié)果還是以約束轉(zhuǎn)化的 frame 為準(zhǔn)。前后腳修改 frame 和約束,這種瞬間的布局變化兩者混合使用不會出錯,因?yàn)樽罱K都修改了 frame;但如果用這種方式進(jìn)行動畫會出現(xiàn)偏差,因?yàn)閮烧咧g的影響是單方向的,必然會造成狀態(tài)的不連續(xù),這是可以預(yù)見的;在這種情況下,如果希望動畫完全符合你的預(yù)期,必須保證約束與 frame 是匹配的,但這樣一來,修改 frame 后還得修改約束,實(shí)在沒必要,使用 Auto Layout 就老老實(shí)實(shí)地使用約束吧。
來看看例子:
@IBOutlet weak var testView: UIView!
@IBOutlet weak var centerXConstraint: NSLayoutConstraint!
var center = testView.center //假設(shè)此時 center.x 的值為160
center.x += 10
testView.center = center //現(xiàn)在 testView 的 center.x 為 170
centerXConstraint.constant += 10 //現(xiàn)在 testView 的 center.x 依然為 170,因?yàn)橹苯有薷?frame 并不影響約束,約束只參照約束
centerXConstraint.constant += 10 //現(xiàn)在 testView 的 center.x 為 180
testView.center.x += 10 // 現(xiàn)在 testView 的 center.x 為 190,但從 Auto Layout 的角度看依然是 180
//動畫從 190 變化到 200
UIView.animateWithDuration(0.5, animations: {
testView.center.x += 10
})
//盡管從 Auto Layout 的角度看 center.x 是 180,修改約束后該值為 190,這個動畫應(yīng)該是從 180 變化到 190,但實(shí)際動畫是從當(dāng)前的 200 變化到 190。
UIView.animateWithDuration(0.5, animations: {
self.centerXConstraint.constant += 10
self.testView.superView.layoutIfNeeded()
})
順便提一下使用 Auto Layout 做動畫的方法。UIView 中更新布局的相關(guān)方法:
Laying out SubViews
- layoutSubviews() //不要直接調(diào)用,如果需要強(qiáng)制更新布局,調(diào)用下面的 setNeedsLayout()
- setNeedsLayout() //標(biāo)記布局需要在下一個周期更新
- layoutIfNeeded() //立刻更新布局
Triggering Auto Layout
- setNeedsUpdateConstraints() //標(biāo)記約束需要在稍后更新,系統(tǒng)會調(diào)用下面的 updateConstraints()方法,修改多個約束后調(diào)用該方法批量更新有助于提升性能
- updateConstraints() //更新調(diào)用該方法的視圖的約束
- updateConstraintsIfNeeded() //更新調(diào)用該方法的視圖以及其子視圖的約束
使用 Auto Layout 時提交動畫與普通的動畫沒有什么區(qū)別,在 Block 里修改相關(guān)屬性,只不過最后需要調(diào)用layoutIfNeeded()立即更新布局,其他方法無效。在網(wǎng)上的一些例子里,也有將修改的步驟放在 Block 之外,起初我搜索到的就是這樣的方式,為了強(qiáng)制保持一致的代碼風(fēng)格,我現(xiàn)在回到了下面的風(fēng)格。
UIView.animateWithDuration(0.5, animations: {
....../*修改 view 的約束*/
view.superView.layoutIfNeeded() //立刻更新其下的子視圖的布局
})
很多時候我們喜歡在viewDidLoad()做一些設(shè)置,但此時還沒有開始布局視圖,如果你希望在此修改約束,記得調(diào)用layoutIfNeeded()方法立刻更新布局使得修改生效。
約束(contraint)的擁有者
我一直覺得使用 AutoLayout 的另外一個重大障礙是找出需要的約束。在 IB 中添加的約束可以在實(shí)現(xiàn)文件里用 IBOutlet 來引用,但可能不會引用每一條約束,或者你是在代碼里添加的呢,所以第一個問題是,怎么找出要修改的約束?約束往往涉及兩個視圖,是否在這兩個視圖上都保存了一份呢,要修改是否需要修改兩份?否。約束保存在兩個視圖最近的父類視圖中或者兩者中層級比較高的那個視圖,事實(shí)上你如果將約束添加到兩者中層級較低的那個視圖會出現(xiàn)錯誤。這是由自動布局的機(jī)制決定的,布局更新的順序是從上到下,從外到內(nèi),在更新布局時需要根據(jù)視圖上的約束對其下的子視圖進(jìn)行布局,添加到子視圖上顯然不利于布局的計算。從另外一個角度講,視圖只會保存它的子視圖相關(guān)的約束,以及參與對象中較高層次是自身的約束,比如設(shè)定self.height = self.width * 0.5這種約束。
知道了地方還需要找到指定的約束,從下面方法的參數(shù)可以看到,如果希望直接對比視圖來查找往往需要一番轉(zhuǎn)換才行,而且還需要對比兩個參數(shù) view1 和 view2,這實(shí)在是很不方便。
init(item view1: AnyObject, attribute attr1: NSLayoutAttribute, relatedBy relation: NSLayoutRelation, toItem view2: AnyObject?, attribute attr2: NSLayoutAttribute, multiplier multiplier: CGFloat, constant c: CGFloat)
約束還有一個屬性var identifier: String?用于標(biāo)記,這在 Debug 時面對超長 Log 時非常省心,這里設(shè)定該值用于查找能夠省點(diǎn)力氣。相比使用 frame 時可以一步修改時的便捷,尋找約束的過程可能就耗盡了你對 AutoLayout 的向往。
另外,從 iOS 8 開始添加約束不必再用view.addConstraint(constraint)這種方法了,如前面所說,這種方法必須將約束添加到最近的父視圖或是參與約束中層級較高的那個視圖中,在 iOS 8 里,NSLayoutConstraint類添加的var active: Bool屬性可以自動調(diào)用相關(guān)視圖的addConstraint:和removeConstraint:方法,不必需要我們操心約束的正確擁有者了。新生成的約束該屬性為false。
另外 iOS 8 還添加了以下兩個批量處理的方法:
class func activateConstraints(_ constraints: [NSLayoutConstraint])
class func deactivateConstraints(_ constraints: [NSLayoutConstraint])
優(yōu)先級 Priority
我剛開始接觸 AutoLayout 的時候知道約束可以是不等式,只要所有的約束有解且唯一就可以保證布局正常,但不知約束的優(yōu)先級有何用處。約束的優(yōu)先級值的范圍為半開區(qū)間(0,1000]之間的 Float,優(yōu)先級值為1000時表示該約束必須滿足,其他的值表示該約束是可選的。在 storyboard 里可以看到優(yōu)先級約定了這么幾個常量:
Required 1000
Hight 750
Low 250
優(yōu)先級值為1000時該值不可再變化,可選約束的優(yōu)先級值可以在(0,1000)之間隨意變化,不能為1000,必須滿足的約束和可選的約束一旦生成了就不能轉(zhuǎn)化到對方的陣營里去。如果所有的 Required 約束不能唯一確定布局,就會從優(yōu)先級次高的約束中補(bǔ)充,依然不能唯一確定布局的話,再從次級的約束中補(bǔ)充。所以不妨換一種思維方式,不需要所有的約束都是 Required 級別的,只要保證優(yōu)先級靠前的約束能唯一確定布局就可以了。
由于優(yōu)先級在1000以下的約束都是可選的,可以針對不同的布局需求添加多個可選約束而且不會引起約束沖突,通過調(diào)整這些可選約束的優(yōu)先級可以實(shí)現(xiàn)不同的布局。

上面的例子來自這篇文章 Animating Autolayout Constraints,這篇文章的最后一次修改中通過分別修改了兩個約束的 constant 和優(yōu)先級來實(shí)現(xiàn)這個動畫。為了讓這個示例更典型一點(diǎn),這里也稍微修改一下原文的實(shí)現(xiàn),僅僅修改其中一個約束的優(yōu)先級就可以實(shí)現(xiàn)這個動畫(黃藍(lán)視圖的間距會稍有不同)。實(shí)現(xiàn)方法:添加如下的的約束條件,只保證兩個視圖之間的距離約束是必須實(shí)現(xiàn)的,相對于尾部的距離約束都是可選的,哪個優(yōu)先級高就滿足哪個。這里的兩個可選約束優(yōu)先級一樣,是無法確定唯一布局的,必須打破這個局面。
yellowView.Trailing = 1.0 * superView.TrailingMargin (priority = 750)
blueView.Trailing = 1.0 * superView.TrailingMargin (priority = 750)
blueView.Leading = 1.0 * yellowView.Trailing + 18(priority = 1000)//視圖間的標(biāo)準(zhǔn)間距為8,這里為了將 blueView 擠出屏幕,多加了10個單位
另外還有個前提約束:yellowView.width = 1.0 * blueView.width (priority = 1000)
在 Switch 的響應(yīng)方法中更改其中一個尾部約束的優(yōu)先級值,此時滿足上面三個約束中優(yōu)先級最高的兩個約束就可以唯一確定布局,這樣當(dāng)兩個可選布局的優(yōu)先值的排位不一樣時就形成了兩種布局:
func updateConstraintsForMode {
if (self.modeSwitch.isOn) {
self.blueViewTrailingConstraint.priority = UILayoutPriorityDefaultHigh + 1
} else {
self.blueViewTrailingConstraint.priority = UILayoutPriorityDefaultHigh - 1
}
}
如果通過更改約束本身的構(gòu)成條件來完成上面的效果,最簡單的辦法是去掉 yellowView 與父視圖的尾部距離約束,然后修改blueViewTrailingConstraint的常量值。這種方法可以說是非常的 frame 風(fēng)格,也是很容易想到的。孰優(yōu)孰劣不好說,這個例子就算是給你提供另外一種可能吧。
優(yōu)先級在一些擁有固有尺寸(intrinsicContentSize)的視圖上運(yùn)用得比較多,像 UILabel,UIButton,UITextField 這類為了優(yōu)先保證內(nèi)容顯示完整的控件,在 storyboard 里添加約束時僅僅需要添加兩個位置相關(guān)的約束就可以了。 在這里可以看到相關(guān)的討論:Priority: Content hugging vs Content compression resistance。
約束系數(shù) Multiplier
約束的組成:

前面說過 AutoLayout 的真正優(yōu)勢是自動化。常見的場景是,比如多個子視圖的 centerX 相對于父視圖的 centerX 保持一定的距離,該距離與子視圖在隊列中的位置有關(guān),其中一個視圖在該方向的位置發(fā)生變化時,后面的視圖會自動更新位置,使用 frame 是難以如此便捷地做到的。但有時候考慮到某個子視圖可能會移除,這樣需要重新配置約束,覺得麻煩,怎么辦,回到 frame 的方法,每個子視圖單獨(dú)與父視圖配置約束,這樣不影響其他子視圖。當(dāng)屏幕旋轉(zhuǎn)時如果父視圖的寬度發(fā)生了變化,如何自動維持這個規(guī)律?我剛開始使用 AutoLayout 時還不適應(yīng)這種布局方式,僅僅只是將 frame 翻譯為對應(yīng)的約束,但采用的手法卻是將對應(yīng)的距離轉(zhuǎn)化為 constant,比如下面這種:
subView.centerX = 1.0 X containerView.centerX + (i * containerView.frame.width)
然而當(dāng)父視圖寬度變化時卻發(fā)現(xiàn)子視圖并沒有自動更新位置。問題在哪?搞錯了這個約束方程式的變量,constant 設(shè)定后它就不會變了,應(yīng)該在父視圖的 centerX 這個變量上做文章。我默默地將multiplier設(shè)置為1后的約束盡管也發(fā)生了變化(很微量往往察覺不到),但沒有達(dá)到預(yù)期,應(yīng)該這么做:
subView.centerX = (2 * i + 1) X containerView.centerX + 0
這樣每一個子視圖都會隨著父視圖的 centerX 的變化自動更新自己的位置。具體例子可以參考這里。
當(dāng)你需要設(shè)定一些特別的比例時,比如 3/7, 7/13 之類,在代碼中可以很方便地就這樣原封不動地交給算式表達(dá)式來計算,在 storyboar 里怎么弄呢,3:7, 7:13 這樣就可以了。