自動(dòng)布局在 OS X 10.7 中被引進(jìn),一年后在 iOS 6 中也可以用了。不久在 iOS 7 中的程序?qū)?huì)有望設(shè)置全局字體大小,因此除了不同的屏幕大小和方向,用戶界面布局也需要更大的靈活性。Apple 也在自動(dòng)布局上花了很大功夫,所以如果你還沒做過這一塊,那么現(xiàn)在就是接觸這個(gè)技術(shù)的好時(shí)機(jī)。
很多開發(fā)者在第一次嘗試使用這個(gè)技術(shù)時(shí)都非常掙扎,因?yàn)橛?Xcode 4 的 Interface Builder 建立 constraint-based layouts 體驗(yàn)非常糟糕,但不要因?yàn)檫@個(gè)而灰心。自動(dòng)布局其實(shí)比現(xiàn)在 Interface Builder 所支持的要好很多。Xcode 5 在這塊中將會(huì)帶來重要的變化。
這篇文章不是用來介紹 Auto Layout 的。如果你還沒用過它,那還是先去 WWDC 2012 看看基礎(chǔ)教程吧。(202 – Introduction to Auto Layout for iOS and OS X, 228 – Best Practices for Mastering Auto Layout, 232 – Auto Layout by Example)。
相反我們會(huì)專注于一些高級(jí)的使用技巧和方法,這將會(huì)讓你使用自動(dòng)布局的時(shí)候效率更高,(開發(fā))生活更幸福。大多數(shù)內(nèi)容在 WWDC 會(huì)議中都有提到,但它們都是在日常工作中容易被審查或遺忘的。
首先我們總結(jié)一下自動(dòng)布局將視圖顯示到屏幕上的步驟。當(dāng)你根據(jù)自動(dòng)布局盡力寫出你想要的布局種類時(shí),特別是高級(jí)的使用情況和動(dòng)畫,這有利于后退一步,并回憶布局過程是怎么工作的。
和 springs,struts 比起來,在視圖被顯示之前,自動(dòng)布局引入了兩個(gè)額外的步驟:更新約束 (updating constraints) 和布局視圖 (laying out views)。每一步都是依賴前一步操作的;顯示依賴于布局視圖,布局視圖依賴于更新約束。
第一步:更新約束,可以被認(rèn)為是一個(gè)“計(jì)量傳遞 (measurement pass)”。這是自下而上(從子視圖到父視圖)發(fā)生的,它為布局準(zhǔn)備好必要的信息,而這些布局將在實(shí)際設(shè)置視圖的 frame 時(shí)被傳遞過去并被使用。你可以通過調(diào)用 setNeedsUpdateConstraints 來觸發(fā)這個(gè)操作,同時(shí),你對(duì)約束條件系統(tǒng)做出的任何改變都將自動(dòng)觸發(fā)這個(gè)方法。無論如何,通知自動(dòng)布局關(guān)于自定義視圖中任何可能影響布局的改變是非常有用的。談到自定義視圖,你可以在這個(gè)階段重寫 updateConstraints 來為你的視圖增加需要的本地約束。
第二步:布局,這是個(gè)自上而下(從父視圖到子視圖)的過程,這種布局操作實(shí)際上是通過設(shè)置 frame(在 OS X 中)或者 center 和 bounds(在 iOS 中)將約束條件系統(tǒng)的解決方案應(yīng)用到視圖上。你可以通過調(diào)用 setNeedsLayout 來觸發(fā)一個(gè)操作請(qǐng)求,這并不會(huì)立刻應(yīng)用布局,而是在稍后再進(jìn)行處理。因?yàn)樗械牟季终?qǐng)求將會(huì)被合并到一個(gè)布局操作中去,所以你不需要為經(jīng)常調(diào)用這個(gè)方法而擔(dān)心。
你可以調(diào)用 layoutIfNeeded / layoutSubtreeIfNeeded(分別針對(duì) iOS / OS X)來強(qiáng)制系統(tǒng)立即更新視圖樹的布局。如果你下一步操作依賴于更新后視圖的 frame,這將非常有用。在你自定義的視圖中,你可以重寫 layoutSubviews / layout 來獲得控制布局變化的所有權(quán),我們稍后將展示使用方法。
最終,不管你是否用了自動(dòng)布局,顯示器都會(huì)自上而下將渲染后的視圖傳遞到屏幕上,你也可以通過調(diào)用 setNeedsDisplay 來觸發(fā),這將會(huì)導(dǎo)致所有的調(diào)用都被合并到一起推遲重繪。重寫熟悉的 drawRect:能夠讓我們獲得自定義視圖中顯示過程的所有權(quán)。
既然每一步都是依賴前一步操作的,如果有任何布局的變化還沒實(shí)行的話,顯示操作將會(huì)觸發(fā)一個(gè)布局行為。類似地,如果約束條件系統(tǒng)中存在沒有實(shí)行的改變,布局變化也將會(huì)觸發(fā)更新約束條件。
需要牢記的是,這三步并不是單向的?;诩s束條件的布局是一個(gè)迭代的過程,布局操作可以基于之前的布局方案來對(duì)約束做出更改,而這將再次觸發(fā)約束的更新,并緊接另一個(gè)布局操作。這可以被用來創(chuàng)建高級(jí)的自定義視圖布局,但是如果你每一次調(diào)用的自定義 layoutSubviews 都會(huì)導(dǎo)致另一個(gè)布局操作的話,你將會(huì)陷入到無限循環(huán)的麻煩中去。
當(dāng)創(chuàng)建一個(gè)自定義視圖時(shí),你需要知道關(guān)于自動(dòng)布局的這些事情:具體指定一個(gè)恰當(dāng)?shù)墓逃袃?nèi)容尺寸 (intrinsic content size),區(qū)分開視圖的 frame 和 alignment rect,啟動(dòng) baseline-aligned 布局,如何 hook 到布局過程中,我們將會(huì)逐一了解這些部分。
固有內(nèi)容尺寸是一個(gè)視圖期望為其顯示特定內(nèi)容得到的大小。比如,UILabel 有一個(gè)基于字體的首選高度,一個(gè)基于字體和顯示文本的首選寬度。UIProgressView 僅有一個(gè)基于其插圖的首選高度,但沒有首選寬度。一個(gè)沒有格式的 UIView 既沒有首選寬度也沒有首選高度。
你需要根據(jù)想要顯示的內(nèi)容來決定你的自定義視圖是否具有一個(gè)固有內(nèi)容尺寸,如果有的話,它是在哪個(gè)尺度上固有。
為了在自定義視圖中實(shí)現(xiàn)固有內(nèi)容尺寸,你需要做兩件事:重寫 intrinsicContentSize 為內(nèi)容返回恰當(dāng)?shù)拇笮?,無論何時(shí)有任何會(huì)影響固有內(nèi)容尺寸的改變發(fā)生時(shí),調(diào)用 invalidateIntrinsicContentSize。如果這個(gè)視圖只有一個(gè)方向的尺寸設(shè)置了固有尺寸,那么為另一個(gè)方向的尺寸返回 UIViewNoIntrinsicMetric / NSViewNoIntrinsicMetric。
需要注意的是,固有內(nèi)容尺寸必須是獨(dú)立于視圖 frame 的。例如,不可能返回一個(gè)基于 frame 高度或?qū)挾鹊奶囟ǜ邔挶鹊墓逃袃?nèi)容尺寸。
譯者注 我理解為壓縮阻力和內(nèi)容吸附性,實(shí)在是想不到更貼切的名稱了。壓縮阻力是控制視圖在兩個(gè)方向上的收縮性,內(nèi)容吸附性是當(dāng)視圖的大小改變時(shí),它會(huì)盡量讓視圖靠近它的固有內(nèi)容尺寸
每個(gè)視圖在兩個(gè)方向上都分配有內(nèi)容壓縮阻力優(yōu)先級(jí)和內(nèi)容吸附性優(yōu)先級(jí)。只有當(dāng)視圖定義了固有內(nèi)容尺寸時(shí)這些屬性才能起作用,如果沒有定義內(nèi)容大小,那就沒法阻止被壓縮或者吸附了。
在后臺(tái)中,固有內(nèi)容尺寸和這些優(yōu)先值被轉(zhuǎn)換為約束條件。一個(gè)固有內(nèi)容尺寸為 {100,30} 的 label,水平/垂直壓縮阻力優(yōu)先值為 750,水平/垂直的內(nèi)容吸附性優(yōu)先值為 250,這四個(gè)約束條件將會(huì)生成:
H:[label(<=100@250)]
H:[label(>=100@750)]
V:[label(<=30@250)]
V:[label(>=30@750)]
如果你不熟悉上面約束條件所使用的可視格式語言,你可以到 Apple 文檔 中了解。記住,這些額外的約束條件對(duì)了解自動(dòng)布局的行為產(chǎn)生了隱式的幫助,同時(shí)也更好理解它的錯(cuò)誤信息。
自動(dòng)布局并不會(huì)操作視圖的 frame,但能作用于視圖的 alignment rect。大家很容易忘記它們之間細(xì)微的差別,因?yàn)樵诤芏嗲闆r下,它們是相同的。但是alignment rect 實(shí)際上是一個(gè)強(qiáng)大的新概念:從一個(gè)視圖的視覺外觀解耦出視圖的 layout alignment edges。
比如,一個(gè)自定義 icon 類型的按鈕比我們期望點(diǎn)擊目標(biāo)還要小的時(shí)候,這將會(huì)很難布局。當(dāng)插圖顯示在一個(gè)更大的 frame 中時(shí),我們將不得不了解它顯示的大小,并且相應(yīng)調(diào)整按鈕的 frame,這樣 icon 才會(huì)和其他界面元素排列好。當(dāng)我們想要在內(nèi)容的周圍繪制像 badges,陰影,倒影的裝飾時(shí),也會(huì)發(fā)生同樣的情況。
我們可以使用 alignment rect 簡單的定義需要用來布局的矩形。在大多數(shù)情況下,你僅需要重寫 alignmentRectInsets 方法,這個(gè)方法允許你返回相對(duì)于 frame 的 edge insets。如果你需要更多控制權(quán),你可以重寫 alignmentRectForFrame: 和 frameForAlignmentRect:。如果你不想減去固定的 insets,而是計(jì)算基于當(dāng)前 frame 的 alignment rect,那么這兩個(gè)方法將會(huì)非常有用。但是你需要確保這兩個(gè)方法是互為可逆的。
關(guān)于這點(diǎn),回憶上面提及到的視圖固有內(nèi)容尺寸引用它的 alignment rect,而不是 frame。這是有道理的,因?yàn)樽詣?dòng)布局直接根據(jù)固有內(nèi)容尺寸產(chǎn)生壓縮阻力和內(nèi)容吸附約束條件。
為了讓使用 NSLayoutAttributeBaseline 屬性的約束條件對(duì)自定義視圖奏效,我們需要做一些額外的工作。當(dāng)然,這只有我們討論的自定義視圖中有類似基準(zhǔn)線的東西時(shí),才有意義。
在 iOS 中,可以通過實(shí)現(xiàn) viewForBaselineLayout 來激活基線對(duì)齊。在這里返回的視圖底邊緣將會(huì)作為 基線。默認(rèn)實(shí)現(xiàn)只是簡單的返回自己,然而自定義的實(shí)現(xiàn)可以返回任何子視圖。在 OS X 中,你不需要返回一個(gè)子視圖,而是重新定義 baselineOffsetFromBottom 返回一個(gè)從視圖底部邊緣開始的 offset,這和在 iOS 中一樣,默認(rèn)實(shí)現(xiàn)都是返回 0。
在自定義視圖中,你能完全控制它子視圖的布局。你可以增加本地約束;根據(jù)內(nèi)容變化需要,你可以改變本地約束;你可以為子視圖調(diào)整布局操作的結(jié)果;或者你可以選擇拋棄自動(dòng)布局。
但確保你明智的使用這個(gè)權(quán)利。大多數(shù)情況下可以簡單地通過為你的子視圖簡單的增加本地約束來處理。
如果我們想用幾個(gè)子視圖組成一個(gè)自定義視圖,我們需要以某種方式布局這些子視圖。在自動(dòng)布局的環(huán)境中,自然會(huì)想到為這些視圖增加本地約束。然而,需要注意的是,這將會(huì)使你自定義的視圖是基于自動(dòng)布局的,這個(gè)視圖不能再被使用于未啟用自動(dòng)布局的 windows 中。最好通過實(shí)現(xiàn) requiresConstraintBasedLayout 返回 YES 明確這個(gè)依賴。
添加本地約束的地方是 updateConstraints。確保在你的實(shí)現(xiàn)中增加任何你需要布局子視圖的約束條件之后,調(diào)用一下 [super updateConstraints]。在這個(gè)方法中,你不會(huì)被允許禁用何約束條件,因?yàn)槟阋呀?jīng)進(jìn)入上面所描述的布局過程的第一步了。如果嘗試著這樣做,將會(huì)產(chǎn)生一個(gè)友好的錯(cuò)誤信息 “programming error”。
如果稍后一個(gè)失效的約束條件發(fā)生了改變的話,你需要立刻移除這個(gè)約束并調(diào)用 setNeedsUpdateConstraints。事實(shí)上,僅在這種情況下你需要觸發(fā)更新約束條件的操作。
如果你不能利用布局約束條件達(dá)到子視圖預(yù)期的布局,你可以進(jìn)一步在 iOS 里重寫 layoutSubviews 或者在 OS X 里面重寫 layout。通過這種方式,當(dāng)約束條件系統(tǒng)得到解決并且結(jié)果將要被應(yīng)用到視圖中時(shí),你便已經(jīng)進(jìn)入到布局過程的第二步。
最極端的情況是不調(diào)用父類的實(shí)現(xiàn),自己重寫全部的 layoutSubviews / layout。這就意味著你在這個(gè)視圖里的視圖樹里拋棄了自動(dòng)布局。從現(xiàn)在起,你可以按喜歡的方式手動(dòng)放置子視圖。
如果你仍然想使用約束條件布局子視圖,你需要調(diào)用 [super layoutSubviews] / [super layout],然后對(duì)布局進(jìn)行微調(diào)。你可以通過這種方式創(chuàng)建那些通過定于約束無法實(shí)現(xiàn)的布,比如,由到視圖大小之間的關(guān)系或是視圖之間間距的關(guān)系來定義的布局。
這方面另一個(gè)有趣的使用案例就是創(chuàng)建一個(gè)布局依賴的視圖樹。當(dāng)自動(dòng)布局完成第一次傳遞并且為自定義視圖的子視圖設(shè)置好 frame 后,你便可以檢查子視圖的位置和大小,并為視圖層級(jí)和(或)約束條件做出調(diào)整。WWDC session 228 – Best Practices for Mastering Auto Layout 有一個(gè)很好的例子。
你也可以在第一次布局操作完成后再?zèng)Q定改變約束條件。比如,如果視圖變得太窄的話,將原來排成一行的子視圖轉(zhuǎn)變成兩行。
- layoutSubviews
{
[super layoutSubviews];
if (self.subviews[0].frame.size.width <= MINIMUM_WIDTH)
{
[self removeSubviewConstraints];
self.layoutRows += 1; [super layoutSubviews];
}
}
- updateConstraints
{
// 根據(jù) self.layoutRows 添加約束...
[super updateConstraints];
}
UILabel 和 NSTextField 對(duì)于多行文本的固有內(nèi)容尺寸是模糊不清的。文本的高度取決于行的寬度,這也是解決約束條件時(shí)需要弄清的問題。為了解決這個(gè)問題,這兩個(gè)類都有一個(gè)叫做 preferredMaxLayoutWidth 的新屬性,這個(gè)屬性指定了行寬度的最大值,以便計(jì)算固有內(nèi)容尺寸。
因?yàn)槲覀兺ǔ2荒芴崆爸肋@個(gè)值,為了獲得正確的值我們需要先做兩步操作。首先,我們讓自動(dòng)布局做它的工作,然后用布局操作結(jié)果的 frame 更新給首選最大寬度,并且再次觸發(fā)布局。
- (void)layoutSubviews
{
[super layoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[super layoutSubviews];
}
第一次調(diào)用 [super layoutSubviews] 是為了獲得 label 的 frame,而第二次調(diào)用是為了改變后更新布局。如果省略第二個(gè)調(diào)用我們將會(huì)得到一個(gè) NSInternalInconsistencyException 的錯(cuò)誤,因?yàn)槲覀兏淖兞烁录s束條件的布局操作,但我們并沒有再次觸發(fā)布局。
我們也可以在 label 子類本身中這樣做:
@implementation MyLabel
- (void)layoutSubviews
{
self.preferredMaxLayoutWidth = self.frame.size.width;
[super layoutSubviews];
}
@end
在這種情況下,我們不需要先調(diào)用 [super layoutSubviews],因?yàn)楫?dāng) layoutSubviews 被調(diào)用時(shí),label 就已經(jīng)有一個(gè) frame 了。
為了在視圖控制器層級(jí)做出這樣的調(diào)整,我們用掛鉤到 viewDidLayoutSubviews。這時(shí)候第一個(gè)自動(dòng)布局操作的 frame 已經(jīng)被設(shè)置,我們可以用它們來設(shè)置首選最大寬度。
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[self.view layoutIfNeeded];
}
最后,確保你沒有給 label 設(shè)置一個(gè)比 label 內(nèi)容壓縮阻力優(yōu)先級(jí)還要高的具體高度約束。否則它將會(huì)取代根據(jù)內(nèi)容計(jì)算出的高度。
說到根據(jù)自動(dòng)布局的視圖動(dòng)畫,有兩個(gè)不同的基本策略:約束條件自身動(dòng)態(tài)化;以及改變約束條件重新計(jì)算 frame,并使用 Core Animation 將 frame 插入到新舊位置之間。
這兩種處理方法不同的是:約束條件自身動(dòng)態(tài)化產(chǎn)生的布局結(jié)果總是符合約束條件系統(tǒng)。與此相反,使用 Core Animation 插入值到新舊 frame 之間會(huì)臨時(shí)違反約束條件。
直接使用約束條件動(dòng)態(tài)化只是在 OS X 上的一種可行策略,并且這對(duì)你能使用的動(dòng)畫有局限性,因?yàn)榧s束條件一旦創(chuàng)建后,只有其常量可以被改變。在 OS X 中你可以在約束條件的常量中使用動(dòng)畫代理來驅(qū)動(dòng)動(dòng)畫,而在 iOS 中,你只能手動(dòng)進(jìn)行控制。另外,這種方法明顯比 Core Animation 方法慢得多,這也使得它暫時(shí)不適合移動(dòng)平臺(tái)。
當(dāng)使用 Core Animation 方法時(shí),即使不使用自動(dòng)布局,動(dòng)畫的工作方式在概念上也是一樣的。不同的是,你不需要手動(dòng)設(shè)置視圖的目標(biāo) frames,取而代之的是修改約束條件并觸發(fā)一個(gè)布局操作為你設(shè)置 frames。在 iOS 中,代替:
[UIView animateWithDuration:1 animations:^{
myView.frame = newFrame;
}];
你現(xiàn)在需要寫:
// 更新約束
[UIView animateWithDuration:1 animations:^{
[myView layoutIfNeeded];
}];
請(qǐng)注意,使用這種方法,你可以對(duì)約束條件做出的改變并不局限于約束條件的常量。你可以刪除約束條件,增加約束條件,甚至使用臨時(shí)動(dòng)畫約束條件。由于新的約束只被解釋一次來決定新的 frames,所以更復(fù)雜的布局改變都是有可能的。
需要記住的是:Core Animation 和 Auto Layout 結(jié)合在一起產(chǎn)生視圖動(dòng)畫時(shí),自己不要接觸視圖的 frame。一旦視圖使用自動(dòng)布局,那么你已經(jīng)將設(shè)置 frame 的責(zé)任交給了布局系統(tǒng)。你的干擾將造成怪異的行為。
這也意味著,如果使用的視圖變換 (transform) 改變了視圖的 frame 的話,它和自動(dòng)布局是無法一起正常使用的??紤]下面這個(gè)例子:
[UIView animateWithDuration:1 animations:^{
myView.transform = CGAffineTransformMakeScale(.5, .5);
}];
通常我們期望這個(gè)方法在保持視圖的中心時(shí),將它的大小縮小到原來的一半。但是自動(dòng)布局的行為是根據(jù)我們建立的約束條件種類來放置視圖的。如果我們將其居中于它的父視圖,結(jié)果便像我們預(yù)想的一樣,因?yàn)閼?yīng)用視圖變換會(huì)觸發(fā)一個(gè)在父視圖內(nèi)居中新 frame 的布局操作。然而,如果我們將視圖的左邊緣對(duì)齊到另一個(gè)視圖,那么這個(gè) alignment 將會(huì)粘連住,并且中心點(diǎn)將會(huì)移動(dòng)。
不管怎么樣,即使最初的結(jié)果跟我們預(yù)想的一樣,像這樣通過約束條件將轉(zhuǎn)換應(yīng)用到視圖布局上并不是一個(gè)好主意。視圖的 frame 沒有和約束條件同步,也將導(dǎo)致怪異的行為。
如果你想使用 transform 來產(chǎn)生視圖動(dòng)畫或者直接使它的 frame 動(dòng)態(tài)化,最干凈利索的技術(shù)是將這個(gè)視圖嵌入到一個(gè)視圖容器內(nèi),然后你可以在容器內(nèi)重寫 layoutSubviews,要么選擇完全脫離自動(dòng)布局,要么僅僅調(diào)整它的結(jié)果。舉個(gè)例子,如果我們?cè)谖覀兊娜萜鲀?nèi)建立一個(gè)子視圖,它根據(jù)容器的頂部和左邊緣自動(dòng)布局,當(dāng)布局根據(jù)以上的設(shè)置縮放轉(zhuǎn)換后我們可以調(diào)整它的中心:
- (void)layoutSubviews
{
[super layoutSubviews];
static CGPoint center = {0,0};
if (CGPointEqualToPoint(center, CGPointZero)) {
// 在初次布局后獲取中心點(diǎn)
center = self.animatedView.center;
} else {
// 將中心點(diǎn)賦回給動(dòng)畫視圖
self.animatedView.center = center;
}
}
如果我們將 animatedView 屬性暴露為 IBOutlet,我們甚至可以使用 Interface Builder 里面的容器,并且使用約束條件放置它的的子視圖,同時(shí)還能夠根據(jù)固定的中心應(yīng)用縮放轉(zhuǎn)換。
當(dāng)談到調(diào)試自動(dòng)布局,OS X 比 iOS 還有一個(gè)重要的優(yōu)勢。在 OS X 中,你可以利用 Instrument 的 Cocoa Layout 模板,或者是 NSWindow 的 visualizeConstraints: 方法。而且 NSView 有一個(gè) identifier 屬性,為了獲得更多可讀的自動(dòng)布局錯(cuò)誤信息,你可以在 Interface Builder 或代碼里面設(shè)置這個(gè)屬性。
如果我們?cè)?iOS 中遇到不可滿足的約束條件,我們只能在輸出的日志中看到視圖的內(nèi)存地址。尤其是在更復(fù)雜的布局中,有時(shí)很難辨別出視圖的哪一部分出了問題。然而,在這種情況下,還有幾種方法可以幫到我們。
首先,當(dāng)你在不可滿足的約束條件錯(cuò)誤信息中看到 NSLayoutResizingMaskConstraints 時(shí),你肯定忘了為你某一個(gè)視圖設(shè)定 translatesAutoResizingMaskIntoConstraints 為 NO。Interface Builder 中會(huì)自動(dòng)設(shè)置,但是使用代碼時(shí),你需要為所有的視圖手動(dòng)設(shè)置。
如果不是很明確是哪個(gè)視圖導(dǎo)致的問題,你就需要通過內(nèi)存地址來辨認(rèn)視圖。最簡單的方法是使用調(diào)試控制臺(tái)。你可以打印視圖本身或它父視圖的描述,甚至遞歸描述的樹視圖。這通常會(huì)提示你需要處理哪個(gè)視圖。
(lldb) po 0x7731880
$0 = 124983424 <UIView: 0x7731880; frame = (90 -50; 80 100);
layer = <CALayer: 0x7731450>>
(lldb) po [0x7731880 superview]
$2 = 0x07730fe0 <UIView: 0x7730fe0; frame = (32 128; 259 604);
layer = <CALayer: 0x7731150>>
(lldb) po [[0x7731880 superview] recursiveDescription]
$3 = 0x07117ac0 <UIView: 0x7730fe0; frame = (32 128; 259 604); layer = <CALayer: 0x7731150>>
| <UIView: 0x7731880; frame = (90 -50; 80 100); layer = <CALayer: 0x7731450>>
| <UIView: 0x7731aa0; frame = (90 101; 80 100); layer = <CALayer: 0x7731c60>>
一個(gè)更直觀的方法是在控制臺(tái)修改有問題的視圖,這樣你可以在屏幕上標(biāo)注出來。比如,你可以改變它的背景顏色:
(lldb) expr ((UIView *)0x7731880).backgroundColor = [UIColor purpleColor]
確保重新執(zhí)行你的程序,否則改變不會(huì)在屏幕上顯示出來。還要注意將內(nèi)存地址轉(zhuǎn)換為 (UIView *) ,以及額外的圓括號(hào),這樣我們就可以使用點(diǎn)操作。另外,你當(dāng)然也可以通過發(fā)送消息來實(shí)現(xiàn):
(lldb) expr [(UIView *)0x7731880 setBackgroundColor:[UIColor purpleColor]]
另一種方法是使用 Instrument 的 allocation 模板,根據(jù)圖表分析。一旦你從錯(cuò)誤消息中得到內(nèi)存地址(運(yùn)行 Instruments 時(shí),你從 Console 應(yīng)用中獲得的錯(cuò)誤消息),你可以將 Instrument 的詳細(xì)視圖切換到 Objects List 頁面,并且用 Cmd-F 搜索那個(gè)內(nèi)存地址。這將會(huì)為你顯示分配視圖對(duì)象的方法,這通常是一個(gè)很好的暗示(至少對(duì)那些由代碼創(chuàng)建的視圖來說是這樣的)。
你也可以通過改進(jìn)錯(cuò)誤信息本身,來更容易地在 iOS 中弄懂不可滿足的約束條件錯(cuò)誤到底在哪里。我們可以在一個(gè) category 中重寫 NSLayoutConstraint 的描述,并且將視圖的 tags 包含進(jìn)去:
@implementation NSLayoutConstraint (AutoLayoutDebugging)
#ifdef DEBUG
- (NSString *)description
{
NSString *description = super.description;
NSString *asciiArtDescription = self.asciiArtDescription;
return [description stringByAppendingFormat:@" %@ (%@, %@)",
asciiArtDescription, [self.firstItem tag], [self.secondItem tag]];
}
#endif
@end
如果整數(shù)的 tag 屬性信息不夠的話,我們還可以得到更多新奇的東西,并且在視圖類中增加我們自己命名的屬性,然后可以打印到錯(cuò)誤消息中。我們甚至可以在 Interface Builder 中,使用 identity 檢查器中的 “User Defined Runtime Attributes” 為自定義屬性分配值。
@interface UIView (AutoLayoutDebugging)
- (void)setAbc_NameTag:(NSString *)nameTag;
- (NSString *)abc_nameTag;
@end
@implementation UIView (AutoLayoutDebugging)
- (void)setAbc_NameTag:(NSString *)nameTag
{
objc_setAssociatedObject(self, "abc_nameTag", nameTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)abc_nameTag
{
return objc_getAssociatedObject(self, "abc_nameTag");
}
@end
@implementation NSLayoutConstraint (AutoLayoutDebugging)
#ifdef DEBUG
- (NSString *)description
{
NSString *description = super.description;
NSString *asciiArtDescription = self.asciiArtDescription;
return [description stringByAppendingFormat:@" %@ (%@, %@)", asciiArtDescription, [self.firstItem abc_nameTag], [self.secondItem abc_nameTag]];
}
#endif
@end
通過這種方法錯(cuò)誤消息變得更可讀,并且你不需要找出內(nèi)存地址對(duì)應(yīng)的視圖。然而,對(duì)你而言,你需要做一些額外的工作以確保每次為視圖分配的名字都是有意義。
Daniel 提出了另一個(gè)很巧妙的方法,可以為你提供更好的錯(cuò)誤消息并且不需要額外的工作:對(duì)于每個(gè)布局約束條件,都需要將調(diào)用棧的標(biāo)志融入到錯(cuò)誤消息中。這樣就很容易看出來問題涉及到的約束了。要做到這一點(diǎn),你需要 swizzle UIView 或者 NSView 的 addConstraint: / addConstraints: 方法,以及布局約束的 description 方法。在添加約束的方法中,你需要為每個(gè)約束條件關(guān)聯(lián)一個(gè)對(duì)象,這個(gè)對(duì)象描述了當(dāng)前調(diào)用棧堆棧的第一個(gè)棧頂信息(或者任何你從中得到的信息):
static void AddTracebackToConstraints(NSArray *constraints)
{
NSArray *a = [NSThread callStackSymbols];
NSString *symbol = nil;
if (2 < [a count]) {
NSString *line = a[2];
// Format is
// 1 2 3 4 5
// 012345678901234567890123456789012345678901234567890123456789
// 8 MyCoolApp 0x0000000100029809 -[MyViewController loadView] + 99
//
// Don't add if this wasn't called from "MyCoolApp":
if (59 <= [line length]) {
line = [line substringFromIndex:4];
if ([line hasPrefix:@"My"]) {
symbol = [line substringFromIndex:59 - 4];
}
}
}
for (NSLayoutConstraint *c in constraints) {
if (symbol != nil) {
objc_setAssociatedObject(c, &ObjcioLayoutConstraintDebuggingShort,
symbol, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
objc_setAssociatedObject(c, &ObjcioLayoutConstraintDebuggingCallStackSymbols,
a, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
}
@end
一旦你為每個(gè)約束對(duì)象提供這些信息,你可以簡單的修改 UILayoutConstraint 的描述方法將其包含到輸出日志中。
- (NSString *)objcioOverride_description {
// call through to the original, really
NSString *description = [self objcioOverride_description];
NSString *objcioTag = objc_getAssociatedObject(self, &ObjcioLayoutConstraintDebuggingShort);
if (objcioTag == nil) {
return description;
}
return [description stringByAppendingFormat:@" %@", objcioTag];
}
檢出這個(gè)GitHub倉庫,了解這一技術(shù)的代碼示例。
另一個(gè)常見的問題就是有歧義的布局。如果我們忘記添加一個(gè)約束條件,我們經(jīng)常會(huì)想為什么布局看起來不像我們所期望的那樣。UIView 和 NSView 提供三種方式來查明有歧義的布局:hasAmbiguousLayout,exerciseAmbiguityInLayout,和私有方法 _autolayoutTrace。
顧名思義,如果視圖存在有歧義的布局,那么 hasAmbiguousLayout 返回YES。如果我們不想自己遍歷視圖層并記錄這個(gè)值,可以使用私有方法 _autolayoutTrace。這將返回一個(gè)描述整個(gè)視圖樹的字符串:類似于 recursiveDescription 的輸出(當(dāng)視圖存在有歧義的布局時(shí),這個(gè)方法會(huì)告訴你)。
由于這個(gè)方法是私有的,確保正式產(chǎn)品里面不要包含調(diào)用這個(gè)方法的任何代碼。為了防止你犯這種錯(cuò)誤,你可以在視圖的category中這樣做:
@implementation UIView (AutoLayoutDebugging)
- (void)printAutoLayoutTrace {
#ifdef DEBUG
NSLog(@"%@", [self performSelector:@selector(_autolayoutTrace)]);
#endif
}
@end
_autolayoutTrace 打印的結(jié)果如下:
2013-07-23 17:36:08.920 FlexibleLayout[4237:907]
*<UIWindow:0x7269010>
| *<UILayoutContainerView:0x7381250>
| | *<UITransitionView:0x737c4d0>
| | | *<UIViewControllerWrapperView:0x7271e20>
| | | | *<UIView:0x7267c70>
| | | | | *<UIView:0x7270420> - AMBIGUOUS LAYOUT
| | <UITabBar:0x726d440>
| | | <_UITabBarBackgroundView:0x7272530>
| | | <UITabBarButton:0x726e880>
| | | | <UITabBarSwappableImageView:0x7270da0>
| | | | <UITabBarButtonLabel:0x726dcb0>
正如不可滿足約束條件的錯(cuò)誤消息一樣,我們?nèi)匀恍枰靼状蛴〕龅膬?nèi)存地址所對(duì)應(yīng)的視圖。
另一個(gè)標(biāo)識(shí)出有歧義布局更直觀的方法就是使用 exerciseAmbiguityInLayout。這將會(huì)在有效值之間隨機(jī)改變視圖的 frame。然而,每次調(diào)用這個(gè)方法只會(huì)改變 frame 一次。所以當(dāng)你啟動(dòng)程序的時(shí)候,你根本不會(huì)看到改變。創(chuàng)建一個(gè)遍歷所有視圖層級(jí)的輔助方法是一個(gè)不錯(cuò)的主意,并且讓所有的視圖都有一個(gè)歧義的布局“晃動(dòng) (jiggle)”。
@implementation UIView (AutoLayoutDebugging)
- (void)exerciseAmiguityInLayoutRepeatedly:(BOOL)recursive {
#ifdef DEBUG
if (self.hasAmbiguousLayout) {
[NSTimer scheduledTimerWithTimeInterval:.5
target:self
selector:@selector(exerciseAmbiguityInLayout)
userInfo:nil
repeats:YES];
}
if (recursive) {
for (UIView *subview in self.subviews) {
[subview exerciseAmbiguityInLayoutRepeatedly:YES];
}
}
#endif
} @end
有幾個(gè)有用的 NSUserDefault 選項(xiàng)可以幫助我們調(diào)試、測試自動(dòng)布局。你可以在代碼中設(shè)定,或者你也可以在 scheme editor 中指定它們作為啟動(dòng)參數(shù)。
顧名思義,UIViewShowAlignmentRects和 NSViewShowAlignmentRects 設(shè)置視圖可見的 alignment rects。NSDoubleLocalizedStrings 簡單的獲取并復(fù)制每個(gè)本地化的字符串。這是一個(gè)測試更長語言布局的好方法。最后,設(shè)置 AppleTextDirection 和 NSForceRightToLeftWritingDirection 為 YES,來模擬從右到左的語言。
編者注 如果你不知道怎么在 scheme 中設(shè)置類似 `NSDoubleLocalizedStrings`,這里有一張圖來說明; 
當(dāng)在代碼中設(shè)置視圖和它們的約束條件時(shí)候,一定要記得將 translatesAutoResizingMaskIntoConstraints 設(shè)置為 NO。如果忘記設(shè)置這個(gè)屬性幾乎肯定會(huì)導(dǎo)致不可滿足的約束條件錯(cuò)誤。即使你已經(jīng)用自動(dòng)布局一段時(shí)間了,但還是要小心這個(gè)問題,因?yàn)楹苋菀自诓唤?jīng)意間發(fā)生產(chǎn)生這個(gè)錯(cuò)誤。
當(dāng)你使用 可視化結(jié)構(gòu)語言 (visual format language, VFL) 設(shè)置約束條件時(shí), constraintsWithVisualFormat:options:metrics:views: 方法有一個(gè)很有用的 option 參數(shù)。如果你還沒有用過,請(qǐng)參見文檔。這不同于格式化字符串只能影響一個(gè)視圖,它允許你調(diào)整在一定范圍內(nèi)的視圖。舉個(gè)例子,如果用可視格式語言指定水平布局,那么你可以使用 NSLayoutFormatAlignAllTop 排列可視語言里所有視圖為上邊緣對(duì)齊。
還有一個(gè)使用可視格式語言在父視圖中居中子視圖的小技巧,這技巧利用了不均等約束和可選參數(shù)。下面的代碼在父視圖中水平排列了一個(gè)視圖:
UIView *superview = theSuperView;
NSDictionary *views = NSDictionaryOfVariableBindings(superview, subview);
NSArray *c = [NSLayoutConstraint
constraintsWithVisualFormat:@"V:[superview]-(<=1)-[subview]"]
options:NSLayoutFormatAlignAllCenterX
metrics:nil
views:views];
[superview addConstraints:c];
這利用了 NSLayoutFormatAlignAllCenterX 選項(xiàng)在父視圖和子視圖間創(chuàng)建了居中約束。格式化字符串本身只是一個(gè)虛擬的東西,它會(huì)產(chǎn)生一個(gè)指定的約束,通常情況下只要子視圖是可見的,那么父視圖底部和子視圖頂部邊緣之間的空間就應(yīng)該小于等于1點(diǎn)。你可以顛倒示例中的方向達(dá)到垂直居中的效果。
使用可視格式語言另一個(gè)方便的輔助方法就是我們?cè)谏厦胬又幸呀?jīng)使用過的 NSDictionaryFromVariableBindings 宏指令,你傳遞一個(gè)可變數(shù)量的變量過去,返回得到一個(gè)鍵為變量名的字典。
為了布局任務(wù),你需要一遍一遍的調(diào)試,你可以方便的創(chuàng)建自己的輔助方法。比如,你想要垂直地排列一系列視圖,想要它們垂直方向間距一致,水平方向上所有視圖以它們的左邊緣對(duì)齊,用下面的方法將會(huì)方便很多:
@implementation UIView (AutoLayoutHelpers)
+ leftAlignAndVerticallySpaceOutViews:(NSArray *)views
distance:(CGFloat)distance
{
for (NSUInteger i = 1; i < views.count; i++) {
UIView *firstView = views[i - 1];
UIView *secondView = views[i];
firstView.translatesAutoResizingMaskIntoConstraints = NO;
secondView.translatesAutoResizingMaskIntoConstraints = NO;
NSLayoutConstraint *c1 = constraintWithItem:firstView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:secondView
attribute:NSLayoutAttributeTop
multiplier:1
constant:distance];
NSLayoutConstraint *c2 = constraintWithItem:firstView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:secondView
attribute:NSLayoutAttributeLeading
multiplier:1
constant:0];
[firstView.superview addConstraints:@[c1, c2]];
}
}
@end
同時(shí)也有許多不同的自動(dòng)布局的庫采用了不同的方法來簡化約束條件代碼。
自動(dòng)布局是布局過程中額外的一個(gè)步驟。它需要一組約束條件,并把這些約束條件轉(zhuǎn)換成 frame。因此這自然會(huì)產(chǎn)生一些性能的影響。你需要知道的是,在絕大數(shù)情況下,用來解決約束條件系統(tǒng)的時(shí)間是可以忽略不計(jì)的。但是如果你正好在處理一些性能關(guān)鍵的視圖代碼時(shí),最好還是對(duì)這一點(diǎn)有所了解。
例如,有一個(gè) collection view,當(dāng)新出現(xiàn)一行時(shí),你需要在屏幕上呈現(xiàn)幾個(gè)新的 cell,并且每個(gè) cell 包含幾個(gè)基于自動(dòng)布局的子視圖,這時(shí)你需要注意你的性能了。幸運(yùn)的是,我們不需要用直覺來感受上下滾動(dòng)的性能。啟動(dòng) Instruments 真實(shí)的測量一下自動(dòng)布局消耗的時(shí)間。當(dāng)心 NSISEngine 類的方法。
另一種情況就是當(dāng)你一次顯示大量視圖時(shí)可能會(huì)有性能問題。將約束條件轉(zhuǎn)換成視圖的 frame 時(shí),用來計(jì)算約束的算法是超線性復(fù)雜的。這意味著當(dāng)有一定數(shù)量的視圖時(shí),性能將會(huì)變得非常低下。而這確切的數(shù)目取決于你具體使用情況和視圖配置。但是,給你一個(gè)粗略的概念,在當(dāng)前 iOS 設(shè)備下,這個(gè)數(shù)字大概是 100。你可以讀這兩個(gè)博客帖子了解更多的細(xì)節(jié)。
記住,這些都是極端的情況,不要過早的優(yōu)化,并且避免自動(dòng)布局潛在的性能影響。這樣大多數(shù)情況便不會(huì)有問題。但是如果你懷疑這花費(fèi)了你決定性的幾十毫秒,從而導(dǎo)致用戶界面不完全流暢的話,分析你的代碼,然后你再去考慮用回手動(dòng)設(shè)置 frame 有沒有意義。此外,硬件將會(huì)變得越來越能干,并且Apple也會(huì)繼續(xù)調(diào)整自動(dòng)布局的性能。所以現(xiàn)實(shí)世界中極端情況的性能問題也將隨著時(shí)間減少。
自動(dòng)布局是一個(gè)創(chuàng)建靈活用戶界面的強(qiáng)大功能,這種技術(shù)不會(huì)消失。剛開始使用自動(dòng)布局時(shí)可能會(huì)有點(diǎn)困難,但總會(huì)有柳暗花明的一天。一旦你掌握了這種技術(shù),并且掌握了排錯(cuò)的小技巧,便可庖丁解牛,恍然大悟:這太符合邏輯了。