自動布局在 OS X 10.7 中被引進,一年后在 iOS 6 中也可以用了。不久在 iOS 7 中的程序?qū)型O(shè)置全局字體大小,因此除了不同的屏幕大小和方向,用戶界面布局也需要更大的靈活性。Apple 也在自動布局上花了很大功夫,所以如果你還沒做過這一塊,那么現(xiàn)在就是接觸這個技術(shù)的好時機。
很多開發(fā)者在第一次嘗試使用這個技術(shù)時都非常掙扎,因為用 Xcode 4 的 Interface Builder 建立 constraint-based layouts 體驗非常糟糕,但不要因為這個而灰心。自動布局其實比現(xiàn)在 Interface Builder 所支持的要好很多。Xcode 5 在這塊中將會帶來重要的變化。
這篇文章不是用來介紹 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)。
相反我們會專注于一些高級的使用技巧和方法,這將會讓你使用自動布局的時候效率更高,(開發(fā))生活更幸福。大多數(shù)內(nèi)容在 WWDC 會議中都有提到,但它們都是在日常工作中容易被審查或遺忘的。
首先我們總結(jié)一下自動布局將視圖顯示到屏幕上的步驟。當(dāng)你根據(jù)自動布局盡力寫出你想要的布局種類時,特別是高級的使用情況和動畫,這有利于后退一步,并回憶布局過程是怎么工作的。
和 springs,struts 比起來,在視圖被顯示之前,自動布局引入了兩個額外的步驟:更新約束 (updating constraints) 和布局視圖 (laying out views)。每一步都是依賴前一步操作的;顯示依賴于布局視圖,布局視圖依賴于更新約束。
第一步:更新約束,可以被認(rèn)為是一個“計量傳遞 (measurement pass)”。這是自下而上(從子視圖到父視圖)發(fā)生的,它為布局準(zhǔn)備好必要的信息,而這些布局將在實際設(shè)置視圖的 frame 時被傳遞過去并被使用。你可以通過調(diào)用 setNeedsUpdateConstraints 來觸發(fā)這個操作,同時,你對約束條件系統(tǒng)做出的任何改變都將自動觸發(fā)這個方法。無論如何,通知自動布局關(guān)于自定義視圖中任何可能影響布局的改變是非常有用的。談到自定義視圖,你可以在這個階段重寫 updateConstraints 來為你的視圖增加需要的本地約束。
第二步:布局,這是個自上而下(從父視圖到子視圖)的過程,這種布局操作實際上是通過設(shè)置 frame(在 OS X 中)或者 center 和 bounds(在 iOS 中)將約束條件系統(tǒng)的解決方案應(yīng)用到視圖上。你可以通過調(diào)用 setNeedsLayout 來觸發(fā)一個操作請求,這并不會立刻應(yīng)用布局,而是在稍后再進行處理。因為所有的布局請求將會被合并到一個布局操作中去,所以你不需要為經(jīng)常調(diào)用這個方法而擔(dān)心。
你可以調(diào)用 layoutIfNeeded
/ layoutSubtreeIfNeeded
(分別針對 iOS / OS X)來強制系統(tǒng)立即更新視圖樹的布局。如果你下一步操作依賴于更新后視圖的 frame,這將非常有用。在你自定義的視圖中,你可以重寫 layoutSubviews
/ layout
來獲得控制布局變化的所有權(quán),我們稍后將展示使用方法。
最終,不管你是否用了自動布局,顯示器都會自上而下將渲染后的視圖傳遞到屏幕上,你也可以通過調(diào)用 setNeedsDisplay
來觸發(fā),這將會導(dǎo)致所有的調(diào)用都被合并到一起推遲重繪。重寫熟悉的 drawRect:
能夠讓我們獲得自定義視圖中顯示過程的所有權(quán)。
既然每一步都是依賴前一步操作的,如果有任何布局的變化還沒實行的話,顯示操作將會觸發(fā)一個布局行為。類似地,如果約束條件系統(tǒng)中存在沒有實行的改變,布局變化也將會觸發(fā)更新約束條件。
需要牢記的是,這三步并不是單向的?;诩s束條件的布局是一個迭代的過程,布局操作可以基于之前的布局方案來對約束做出更改,而這將再次觸發(fā)約束的更新,并緊接另一個布局操作。這可以被用來創(chuàng)建高級的自定義視圖布局,但是如果你每一次調(diào)用的自定義 layoutSubviews
都會導(dǎo)致另一個布局操作的話,你將會陷入到無限循環(huán)的麻煩中去。
當(dāng)創(chuàng)建一個自定義視圖時,你需要知道關(guān)于自動布局的這些事情:具體指定一個恰當(dāng)?shù)墓逃袃?nèi)容尺寸 (intrinsic content size),區(qū)分開視圖的 frame 和 alignment rect,啟動 baseline-aligned 布局,如何 hook 到布局過程中,我們將會逐一了解這些部分。
固有內(nèi)容尺寸是一個視圖期望為其顯示特定內(nèi)容得到的大小。比如,UILabel
有一個基于字體的首選高度,一個基于字體和顯示文本的首選寬度。UIProgressView
僅有一個基于其插圖的首選高度,但沒有首選寬度。一個沒有格式的 UIView
既沒有首選寬度也沒有首選高度。
你需要根據(jù)想要顯示的內(nèi)容來決定你的自定義視圖是否具有一個固有內(nèi)容尺寸,如果有的話,它是在哪個尺度上固有。
為了在自定義視圖中實現(xiàn)固有內(nèi)容尺寸,你需要做兩件事:重寫 intrinsicContentSize
為內(nèi)容返回恰當(dāng)?shù)拇笮。瑹o論何時有任何會影響固有內(nèi)容尺寸的改變發(fā)生時,調(diào)用 invalidateIntrinsicContentSize
。如果這個視圖只有一個方向的尺寸設(shè)置了固有尺寸,那么為另一個方向的尺寸返回 UIViewNoIntrinsicMetric
/ NSViewNoIntrinsicMetric
。
需要注意的是,固有內(nèi)容尺寸必須是獨立于視圖 frame 的。例如,不可能返回一個基于 frame 高度或?qū)挾鹊奶囟ǜ邔挶鹊墓逃袃?nèi)容尺寸。
譯者注 我理解為壓縮阻力和內(nèi)容吸附性,實在是想不到更貼切的名稱了。壓縮阻力是控制視圖在兩個方向上的收縮性,內(nèi)容吸附性是當(dāng)視圖的大小改變時,它會盡量讓視圖靠近它的固有內(nèi)容尺寸
每個視圖在兩個方向上都分配有內(nèi)容壓縮阻力優(yōu)先級和內(nèi)容吸附性優(yōu)先級。只有當(dāng)視圖定義了固有內(nèi)容尺寸時這些屬性才能起作用,如果沒有定義內(nèi)容大小,那就沒法阻止被壓縮或者吸附了。
在后臺中,固有內(nèi)容尺寸和這些優(yōu)先值被轉(zhuǎn)換為約束條件。一個固有內(nèi)容尺寸為 {100,30}
的 label,水平/垂直壓縮阻力優(yōu)先值為 750
,水平/垂直的內(nèi)容吸附性優(yōu)先值為 250
,這四個約束條件將會生成:
H:[label(<=100@250)]
H:[label(>=100@750)]
V:[label(<=30@250)]
V:[label(>=30@750)]
如果你不熟悉上面約束條件所使用的可視格式語言,你可以到 Apple 文檔 中了解。記住,這些額外的約束條件對了解自動布局的行為產(chǎn)生了隱式的幫助,同時也更好理解它的錯誤信息。
自動布局并不會操作視圖的 frame,但能作用于視圖的 alignment rect。大家很容易忘記它們之間細(xì)微的差別,因為在很多情況下,它們是相同的。但是alignment rect 實際上是一個強大的新概念:從一個視圖的視覺外觀解耦出視圖的 layout alignment edges。
比如,一個自定義 icon 類型的按鈕比我們期望點擊目標(biāo)還要小的時候,這將會很難布局。當(dāng)插圖顯示在一個更大的 frame 中時,我們將不得不了解它顯示的大小,并且相應(yīng)調(diào)整按鈕的 frame,這樣 icon 才會和其他界面元素排列好。當(dāng)我們想要在內(nèi)容的周圍繪制像 badges,陰影,倒影的裝飾時,也會發(fā)生同樣的情況。
我們可以使用 alignment rect 簡單的定義需要用來布局的矩形。在大多數(shù)情況下,你僅需要重寫 alignmentRectInsets
方法,這個方法允許你返回相對于 frame 的 edge insets。如果你需要更多控制權(quán),你可以重寫 alignmentRectForFrame:
和 frameForAlignmentRect:
。如果你不想減去固定的 insets,而是計算基于當(dāng)前 frame 的 alignment rect,那么這兩個方法將會非常有用。但是你需要確保這兩個方法是互為可逆的。
關(guān)于這點,回憶上面提及到的視圖固有內(nèi)容尺寸引用它的 alignment rect,而不是 frame。這是有道理的,因為自動布局直接根據(jù)固有內(nèi)容尺寸產(chǎn)生壓縮阻力和內(nèi)容吸附約束條件。
為了讓使用 NSLayoutAttributeBaseline
屬性的約束條件對自定義視圖奏效,我們需要做一些額外的工作。當(dāng)然,這只有我們討論的自定義視圖中有類似基準(zhǔn)線的東西時,才有意義。
在 iOS 中,可以通過實現(xiàn) viewForBaselineLayout
來激活基線對齊。在這里返回的視圖底邊緣將會作為 基線。默認(rèn)實現(xiàn)只是簡單的返回自己,然而自定義的實現(xiàn)可以返回任何子視圖。在 OS X 中,你不需要返回一個子視圖,而是重新定義 baselineOffsetFromBottom
返回一個從視圖底部邊緣開始的 offset,這和在 iOS 中一樣,默認(rèn)實現(xiàn)都是返回 0。
在自定義視圖中,你能完全控制它子視圖的布局。你可以增加本地約束;根據(jù)內(nèi)容變化需要,你可以改變本地約束;你可以為子視圖調(diào)整布局操作的結(jié)果;或者你可以選擇拋棄自動布局。
但確保你明智的使用這個權(quán)利。大多數(shù)情況下可以簡單地通過為你的子視圖簡單的增加本地約束來處理。
如果我們想用幾個子視圖組成一個自定義視圖,我們需要以某種方式布局這些子視圖。在自動布局的環(huán)境中,自然會想到為這些視圖增加本地約束。然而,需要注意的是,這將會使你自定義的視圖是基于自動布局的,這個視圖不能再被使用于未啟用自動布局的 windows 中。最好通過實現(xiàn) requiresConstraintBasedLayout
返回 YES 明確這個依賴。
添加本地約束的地方是 updateConstraints
。確保在你的實現(xiàn)中增加任何你需要布局子視圖的約束條件之后,調(diào)用一下 [super updateConstraints]
。在這個方法中,你不會被允許禁用何約束條件,因為你已經(jīng)進入上面所描述的布局過程的第一步了。如果嘗試著這樣做,將會產(chǎn)生一個友好的錯誤信息 “programming error”。
如果稍后一個失效的約束條件發(fā)生了改變的話,你需要立刻移除這個約束并調(diào)用 setNeedsUpdateConstraints
。事實上,僅在這種情況下你需要觸發(fā)更新約束條件的操作。
如果你不能利用布局約束條件達(dá)到子視圖預(yù)期的布局,你可以進一步在 iOS 里重寫 layoutSubviews
或者在 OS X 里面重寫 layout
。通過這種方式,當(dāng)約束條件系統(tǒng)得到解決并且結(jié)果將要被應(yīng)用到視圖中時,你便已經(jīng)進入到布局過程的第二步。
最極端的情況是不調(diào)用父類的實現(xiàn),自己重寫全部的 layoutSubviews / layout
。這就意味著你在這個視圖里的視圖樹里拋棄了自動布局。從現(xiàn)在起,你可以按喜歡的方式手動放置子視圖。
如果你仍然想使用約束條件布局子視圖,你需要調(diào)用 [super layoutSubviews]
/ [super layout]
,然后對布局進行微調(diào)。你可以通過這種方式創(chuàng)建那些通過定于約束無法實現(xiàn)的布,比如,由到視圖大小之間的關(guān)系或是視圖之間間距的關(guān)系來定義的布局。
這方面另一個有趣的使用案例就是創(chuàng)建一個布局依賴的視圖樹。當(dāng)自動布局完成第一次傳遞并且為自定義視圖的子視圖設(shè)置好 frame 后,你便可以檢查子視圖的位置和大小,并為視圖層級和(或)約束條件做出調(diào)整。WWDC session 228 – Best Practices for Mastering Auto Layout 有一個很好的例子。
你也可以在第一次布局操作完成后再決定改變約束條件。比如,如果視圖變得太窄的話,將原來排成一行的子視圖轉(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
對于多行文本的固有內(nèi)容尺寸是模糊不清的。文本的高度取決于行的寬度,這也是解決約束條件時需要弄清的問題。為了解決這個問題,這兩個類都有一個叫做 preferredMaxLayoutWidth
的新屬性,這個屬性指定了行寬度的最大值,以便計算固有內(nèi)容尺寸。
因為我們通常不能提前知道這個值,為了獲得正確的值我們需要先做兩步操作。首先,我們讓自動布局做它的工作,然后用布局操作結(jié)果的 frame 更新給首選最大寬度,并且再次觸發(fā)布局。
- (void)layoutSubviews
{
[super layoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[super layoutSubviews];
}
第一次調(diào)用 [super layoutSubviews]
是為了獲得 label 的 frame,而第二次調(diào)用是為了改變后更新布局。如果省略第二個調(diào)用我們將會得到一個 NSInternalInconsistencyException
的錯誤,因為我們改變了更新約束條件的布局操作,但我們并沒有再次觸發(fā)布局。
我們也可以在 label 子類本身中這樣做:
@implementation MyLabel
- (void)layoutSubviews
{
self.preferredMaxLayoutWidth = self.frame.size.width;
[super layoutSubviews];
}
@end
在這種情況下,我們不需要先調(diào)用 [super layoutSubviews]
,因為當(dāng) layoutSubviews
被調(diào)用時,label 就已經(jīng)有一個 frame 了。
為了在視圖控制器層級做出這樣的調(diào)整,我們用掛鉤到 viewDidLayoutSubviews。這時候第一個自動布局操作的 frame 已經(jīng)被設(shè)置,我們可以用它們來設(shè)置首選最大寬度。
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[self.view layoutIfNeeded];
}
最后,確保你沒有給 label 設(shè)置一個比 label 內(nèi)容壓縮阻力優(yōu)先級還要高的具體高度約束。否則它將會取代根據(jù)內(nèi)容計算出的高度。
說到根據(jù)自動布局的視圖動畫,有兩個不同的基本策略:約束條件自身動態(tài)化;以及改變約束條件重新計算 frame,并使用 Core Animation 將 frame 插入到新舊位置之間。
這兩種處理方法不同的是:約束條件自身動態(tài)化產(chǎn)生的布局結(jié)果總是符合約束條件系統(tǒng)。與此相反,使用 Core Animation 插入值到新舊 frame 之間會臨時違反約束條件。
直接使用約束條件動態(tài)化只是在 OS X 上的一種可行策略,并且這對你能使用的動畫有局限性,因為約束條件一旦創(chuàng)建后,只有其常量可以被改變。在 OS X 中你可以在約束條件的常量中使用動畫代理來驅(qū)動動畫,而在 iOS 中,你只能手動進行控制。另外,這種方法明顯比 Core Animation 方法慢得多,這也使得它暫時不適合移動平臺。
當(dāng)使用 Core Animation 方法時,即使不使用自動布局,動畫的工作方式在概念上也是一樣的。不同的是,你不需要手動設(shè)置視圖的目標(biāo) frames,取而代之的是修改約束條件并觸發(fā)一個布局操作為你設(shè)置 frames。在 iOS 中,代替:
[UIView animateWithDuration:1 animations:^{
myView.frame = newFrame;
}];
你現(xiàn)在需要寫:
// 更新約束
[UIView animateWithDuration:1 animations:^{
[myView layoutIfNeeded];
}];
請注意,使用這種方法,你可以對約束條件做出的改變并不局限于約束條件的常量。你可以刪除約束條件,增加約束條件,甚至使用臨時動畫約束條件。由于新的約束只被解釋一次來決定新的 frames,所以更復(fù)雜的布局改變都是有可能的。
需要記住的是:Core Animation 和 Auto Layout 結(jié)合在一起產(chǎn)生視圖動畫時,自己不要接觸視圖的 frame。一旦視圖使用自動布局,那么你已經(jīng)將設(shè)置 frame 的責(zé)任交給了布局系統(tǒng)。你的干擾將造成怪異的行為。
這也意味著,如果使用的視圖變換 (transform) 改變了視圖的 frame 的話,它和自動布局是無法一起正常使用的。考慮下面這個例子:
[UIView animateWithDuration:1 animations:^{
myView.transform = CGAffineTransformMakeScale(.5, .5);
}];
通常我們期望這個方法在保持視圖的中心時,將它的大小縮小到原來的一半。但是自動布局的行為是根據(jù)我們建立的約束條件種類來放置視圖的。如果我們將其居中于它的父視圖,結(jié)果便像我們預(yù)想的一樣,因為應(yīng)用視圖變換會觸發(fā)一個在父視圖內(nèi)居中新 frame 的布局操作。然而,如果我們將視圖的左邊緣對齊到另一個視圖,那么這個 alignment 將會粘連住,并且中心點將會移動。
不管怎么樣,即使最初的結(jié)果跟我們預(yù)想的一樣,像這樣通過約束條件將轉(zhuǎn)換應(yīng)用到視圖布局上并不是一個好主意。視圖的 frame 沒有和約束條件同步,也將導(dǎo)致怪異的行為。
如果你想使用 transform 來產(chǎn)生視圖動畫或者直接使它的 frame 動態(tài)化,最干凈利索的技術(shù)是將這個視圖嵌入到一個視圖容器內(nèi),然后你可以在容器內(nèi)重寫 layoutSubviews,要么選擇完全脫離自動布局,要么僅僅調(diào)整它的結(jié)果。舉個例子,如果我們在我們的容器內(nèi)建立一個子視圖,它根據(jù)容器的頂部和左邊緣自動布局,當(dāng)布局根據(jù)以上的設(shè)置縮放轉(zhuǎn)換后我們可以調(diào)整它的中心:
- (void)layoutSubviews
{
[super layoutSubviews];
static CGPoint center = {0,0};
if (CGPointEqualToPoint(center, CGPointZero)) {
// 在初次布局后獲取中心點
center = self.animatedView.center;
} else {
// 將中心點賦回給動畫視圖
self.animatedView.center = center;
}
}
如果我們將 animatedView 屬性暴露為 IBOutlet,我們甚至可以使用 Interface Builder 里面的容器,并且使用約束條件放置它的的子視圖,同時還能夠根據(jù)固定的中心應(yīng)用縮放轉(zhuǎn)換。
當(dāng)談到調(diào)試自動布局,OS X 比 iOS 還有一個重要的優(yōu)勢。在 OS X 中,你可以利用 Instrument 的 Cocoa Layout 模板,或者是 NSWindow
的 visualizeConstraints: 方法。而且 NSView
有一個 identifier 屬性,為了獲得更多可讀的自動布局錯誤信息,你可以在 Interface Builder 或代碼里面設(shè)置這個屬性。
如果我們在 iOS 中遇到不可滿足的約束條件,我們只能在輸出的日志中看到視圖的內(nèi)存地址。尤其是在更復(fù)雜的布局中,有時很難辨別出視圖的哪一部分出了問題。然而,在這種情況下,還有幾種方法可以幫到我們。
首先,當(dāng)你在不可滿足的約束條件錯誤信息中看到 NSLayoutResizingMaskConstraints
時,你肯定忘了為你某一個視圖設(shè)定 translatesAutoResizingMaskIntoConstraints
為 NO。Interface Builder 中會自動設(shè)置,但是使用代碼時,你需要為所有的視圖手動設(shè)置。
如果不是很明確是哪個視圖導(dǎo)致的問題,你就需要通過內(nèi)存地址來辨認(rèn)視圖。最簡單的方法是使用調(diào)試控制臺。你可以打印視圖本身或它父視圖的描述,甚至遞歸描述的樹視圖。這通常會提示你需要處理哪個視圖。
(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>>
一個更直觀的方法是在控制臺修改有問題的視圖,這樣你可以在屏幕上標(biāo)注出來。比如,你可以改變它的背景顏色:
(lldb) expr ((UIView *)0x7731880).backgroundColor = [UIColor purpleColor]
確保重新執(zhí)行你的程序,否則改變不會在屏幕上顯示出來。還要注意將內(nèi)存地址轉(zhuǎn)換為 (UIView *)
,以及額外的圓括號,這樣我們就可以使用點操作。另外,你當(dāng)然也可以通過發(fā)送消息來實現(xiàn):
(lldb) expr [(UIView *)0x7731880 setBackgroundColor:[UIColor purpleColor]]
另一種方法是使用 Instrument 的 allocation 模板,根據(jù)圖表分析。一旦你從錯誤消息中得到內(nèi)存地址(運行 Instruments 時,你從 Console 應(yīng)用中獲得的錯誤消息),你可以將 Instrument 的詳細(xì)視圖切換到 Objects List 頁面,并且用 Cmd-F 搜索那個內(nèi)存地址。這將會為你顯示分配視圖對象的方法,這通常是一個很好的暗示(至少對那些由代碼創(chuàng)建的視圖來說是這樣的)。
你也可以通過改進錯誤信息本身,來更容易地在 iOS 中弄懂不可滿足的約束條件錯誤到底在哪里。我們可以在一個 category 中重寫 NSLayoutConstraint
的描述,并且將視圖的 tags 包含進去:
@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
屬性信息不夠的話,我們還可以得到更多新奇的東西,并且在視圖類中增加我們自己命名的屬性,然后可以打印到錯誤消息中。我們甚至可以在 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
通過這種方法錯誤消息變得更可讀,并且你不需要找出內(nèi)存地址對應(yīng)的視圖。然而,對你而言,你需要做一些額外的工作以確保每次為視圖分配的名字都是有意義。
Daniel 提出了另一個很巧妙的方法,可以為你提供更好的錯誤消息并且不需要額外的工作:對于每個布局約束條件,都需要將調(diào)用棧的標(biāo)志融入到錯誤消息中。這樣就很容易看出來問題涉及到的約束了。要做到這一點,你需要 swizzle UIView 或者 NSView 的 addConstraint:
/ addConstraints:
方法,以及布局約束的 description
方法。在添加約束的方法中,你需要為每個約束條件關(guān)聯(lián)一個對象,這個對象描述了當(dāng)前調(diào)用棧堆棧的第一個棧頂信息(或者任何你從中得到的信息):
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
一旦你為每個約束對象提供這些信息,你可以簡單的修改 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];
}
檢出這個GitHub倉庫,了解這一技術(shù)的代碼示例。
另一個常見的問題就是有歧義的布局。如果我們忘記添加一個約束條件,我們經(jīng)常會想為什么布局看起來不像我們所期望的那樣。UIView
和 NSView
提供三種方式來查明有歧義的布局:hasAmbiguousLayout
,exerciseAmbiguityInLayout
,和私有方法 _autolayoutTrace
。
顧名思義,如果視圖存在有歧義的布局,那么 hasAmbiguousLayout
返回YES。如果我們不想自己遍歷視圖層并記錄這個值,可以使用私有方法 _autolayoutTrace。這將返回一個描述整個視圖樹的字符串:類似于 recursiveDescription
的輸出(當(dāng)視圖存在有歧義的布局時,這個方法會告訴你)。
由于這個方法是私有的,確保正式產(chǎn)品里面不要包含調(diào)用這個方法的任何代碼。為了防止你犯這種錯誤,你可以在視圖的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>
正如不可滿足約束條件的錯誤消息一樣,我們?nèi)匀恍枰靼状蛴〕龅膬?nèi)存地址所對應(yīng)的視圖。
另一個標(biāo)識出有歧義布局更直觀的方法就是使用 exerciseAmbiguityInLayout
。這將會在有效值之間隨機改變視圖的 frame。然而,每次調(diào)用這個方法只會改變 frame 一次。所以當(dāng)你啟動程序的時候,你根本不會看到改變。創(chuà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
有幾個有用的 NSUserDefault
選項可以幫助我們調(diào)試、測試自動布局。你可以在代碼中設(shè)定,或者你也可以在 scheme editor 中指定它們作為啟動參數(shù)。
顧名思義,UIViewShowAlignmentRects
和 NSViewShowAlignmentRects
設(shè)置視圖可見的 alignment rects。NSDoubleLocalizedStrings
簡單的獲取并復(fù)制每個本地化的字符串。這是一個測試更長語言布局的好方法。最后,設(shè)置 AppleTextDirection
和 NSForceRightToLeftWritingDirection
為 YES
,來模擬從右到左的語言。
編者注 如果你不知道怎么在 scheme 中設(shè)置類似 `NSDoubleLocalizedStrings`,這里有一張圖來說明; ![pic](../images/3-12.png)
當(dāng)在代碼中設(shè)置視圖和它們的約束條件時候,一定要記得將 translatesAutoResizingMaskIntoConstraints
設(shè)置為 NO。如果忘記設(shè)置這個屬性幾乎肯定會導(dǎo)致不可滿足的約束條件錯誤。即使你已經(jīng)用自動布局一段時間了,但還是要小心這個問題,因為很容易在不經(jīng)意間發(fā)生產(chǎn)生這個錯誤。
當(dāng)你使用 可視化結(jié)構(gòu)語言 (visual format language, VFL) 設(shè)置約束條件時, constraintsWithVisualFormat:options:metrics:views:
方法有一個很有用的 option
參數(shù)。如果你還沒有用過,請參見文檔。這不同于格式化字符串只能影響一個視圖,它允許你調(diào)整在一定范圍內(nèi)的視圖。舉個例子,如果用可視格式語言指定水平布局,那么你可以使用 NSLayoutFormatAlignAllTop
排列可視語言里所有視圖為上邊緣對齊。
還有一個使用可視格式語言在父視圖中居中子視圖的小技巧,這技巧利用了不均等約束和可選參數(shù)。下面的代碼在父視圖中水平排列了一個視圖:
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
選項在父視圖和子視圖間創(chuàng)建了居中約束。格式化字符串本身只是一個虛擬的東西,它會產(chǎn)生一個指定的約束,通常情況下只要子視圖是可見的,那么父視圖底部和子視圖頂部邊緣之間的空間就應(yīng)該小于等于1點。你可以顛倒示例中的方向達(dá)到垂直居中的效果。
使用可視格式語言另一個方便的輔助方法就是我們在上面例子中已經(jīng)使用過的 NSDictionaryFromVariableBindings 宏指令,你傳遞一個可變數(shù)量的變量過去,返回得到一個鍵為變量名的字典。
為了布局任務(wù),你需要一遍一遍的調(diào)試,你可以方便的創(chuàng)建自己的輔助方法。比如,你想要垂直地排列一系列視圖,想要它們垂直方向間距一致,水平方向上所有視圖以它們的左邊緣對齊,用下面的方法將會方便很多:
@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
同時也有許多不同的自動布局的庫采用了不同的方法來簡化約束條件代碼。
自動布局是布局過程中額外的一個步驟。它需要一組約束條件,并把這些約束條件轉(zhuǎn)換成 frame。因此這自然會產(chǎn)生一些性能的影響。你需要知道的是,在絕大數(shù)情況下,用來解決約束條件系統(tǒng)的時間是可以忽略不計的。但是如果你正好在處理一些性能關(guān)鍵的視圖代碼時,最好還是對這一點有所了解。
例如,有一個 collection view,當(dāng)新出現(xiàn)一行時,你需要在屏幕上呈現(xiàn)幾個新的 cell,并且每個 cell 包含幾個基于自動布局的子視圖,這時你需要注意你的性能了。幸運的是,我們不需要用直覺來感受上下滾動的性能。啟動 Instruments 真實的測量一下自動布局消耗的時間。當(dāng)心 NSISEngine
類的方法。
另一種情況就是當(dāng)你一次顯示大量視圖時可能會有性能問題。將約束條件轉(zhuǎn)換成視圖的 frame 時,用來計算約束的算法是超線性復(fù)雜的。這意味著當(dāng)有一定數(shù)量的視圖時,性能將會變得非常低下。而這確切的數(shù)目取決于你具體使用情況和視圖配置。但是,給你一個粗略的概念,在當(dāng)前 iOS 設(shè)備下,這個數(shù)字大概是 100。你可以讀這兩個博客帖子了解更多的細(xì)節(jié)。
記住,這些都是極端的情況,不要過早的優(yōu)化,并且避免自動布局潛在的性能影響。這樣大多數(shù)情況便不會有問題。但是如果你懷疑這花費了你決定性的幾十毫秒,從而導(dǎo)致用戶界面不完全流暢的話,分析你的代碼,然后你再去考慮用回手動設(shè)置 frame 有沒有意義。此外,硬件將會變得越來越能干,并且Apple也會繼續(xù)調(diào)整自動布局的性能。所以現(xiàn)實世界中極端情況的性能問題也將隨著時間減少。
自動布局是一個創(chuàng)建靈活用戶界面的強大功能,這種技術(shù)不會消失。剛開始使用自動布局時可能會有點困難,但總會有柳暗花明的一天。一旦你掌握了這種技術(shù),并且掌握了排錯的小技巧,便可庖丁解牛,恍然大悟:這太符合邏輯了。