先進的自動佈局工具箱

自動佈局在 OS X 10.7 中被引進,一年後在 iOS 6 中也能夠用了。不久在 iOS 7 中的程序將會有望設置全局字體大小,所以除了不一樣的屏幕大小和方向,用戶界面佈局也須要更大的靈活性。Apple 也在自動佈局上花了很大功夫,因此若是你還沒作過這一塊,那麼如今就是接觸這個技術的好時機。html

不少開發者在第一次嘗試使用這個技術時都很是掙扎,由於用 Xcode 4 的 Interface Builder 創建 constraint-based layouts 體驗很是糟糕,但不要由於這個而灰心。自動佈局其實比如今 Interface Builder 所支持的要好不少。Xcode 5 在這塊中將會帶來重要的變化。ios

這篇文章不是用來介紹 Auto Layout 的。若是你還沒用過它,那仍是先去 WWDC 2012 看看基礎教程吧。(202 – Introduction to Auto Layout for iOS and OS X228 – Best Practices for Mastering Auto Layout232 – Auto Layout by Example)。git

相反咱們會專一於一些高級的使用技巧和方法,這將會讓你使用自動佈局的時候效率更高,(開發)生活更幸福。大多數內容在 WWDC 會議中都有提到,但它們都是在平常工做中容易被忽視或遺忘的。github

佈局過程

首先咱們總結一下自動佈局將視圖顯示到屏幕上的步驟。當你根據自動佈局盡力寫出你想要的佈局種類時,特別是高級的使用狀況和動畫,這有利於後退一步,並回憶佈局過程是怎麼工做的。算法

和 springs,struts 比起來,在視圖被顯示以前,自動佈局引入了兩個額外的步驟:更新約束 (updating constraints) 和佈局視圖 (laying out views)。每一步都是依賴前一步操做的;顯示依賴於佈局視圖,佈局視圖依賴於更新約束。spring

第一步:更新約束,能夠被認爲是一個「計量傳遞 (measurement pass)」。這是自下而上(從子視圖到父視圖)發生的,它爲佈局準備好必要的信息,而這些佈局將在實際設置視圖的 frame 時被傳遞過去並被使用。你能夠經過調用 setNeedsUpdateConstraints 來觸發這個操做,同時,你對約束條件系統作出的任何改變都將自動觸發這個方法。不管如何,通知自動佈局關於自定義視圖中任何可能影響佈局的改變是很是有用的。談到自定義視圖,你能夠在這個階段重寫 updateConstraints 來爲你的視圖增長鬚要的本地約束。windows

第二步:佈局,這是個自上而下(從父視圖到子視圖)的過程,這種佈局操做其實是經過設置 frame(在 OS X 中)或者 center 和 bounds(在 iOS 中)將約束條件系統的解決方案應用到視圖上。你能夠經過調用 setNeedsLayout 來觸發一個操做請求,這並不會馬上應用佈局,而是在稍後再進行處理。由於全部的佈局請求將會被合併到一個佈局操做中去,因此你不須要爲常常調用這個方法而擔憂。bash

你能夠調用 layoutIfNeeded / layoutSubtreeIfNeeded(分別針對 iOS / OS X)來強制系統當即更新視圖樹的佈局。若是你下一步操做依賴於更新後視圖的 frame,這將很是有用。在你自定義的視圖中,你能夠重寫 layoutSubviews / layout 來得到控制佈局變化的全部權,咱們稍後將展現使用方法。session

最終,無論你是否用了自動佈局,顯示器都會自上而下將渲染後的視圖傳遞到屏幕上,你也能夠經過調用 setNeedsDisplay 來觸發,這將會致使全部的調用都被合併到一塊兒推遲重繪。重寫熟悉的 drawRect:可以讓咱們得到自定義視圖中顯示過程的全部權。app

既然每一步都是依賴前一步操做的,若是有任何佈局的變化還沒實行的話,顯示操做將會觸發一個佈局行爲。相似地,若是約束條件系統中存在沒有實行的改變,佈局變化也將會觸發更新約束條件。

須要牢記的是,這三步並非單向的。基於約束條件的佈局是一個迭代的過程,佈局操做能夠基於以前的佈局方案來對約束作出更改,而這將再次觸發約束的更新,並緊接另外一個佈局操做。這能夠被用來建立高級的自定義視圖佈局,可是若是你每一次調用的自定義 layoutSubviews 都會致使另外一個佈局操做的話,你將會陷入到無限循環的麻煩中去。

爲自定義視圖啓動自動佈局

當建立一個自定義視圖時,你須要知道關於自動佈局的這些事情:具體指定一個恰當的固有內容尺寸 (intrinsic content size),區分開視圖的 frame 和 alignment rect,啓動 baseline-aligned 佈局,如何 hook 到佈局過程當中,咱們將會逐一瞭解這些部分。

固有內容尺寸(Intrinsic Content Size )

固有內容尺寸是一個視圖指望爲其顯示特定內容獲得的大小。好比,UILabel 有一個基於字體的首選高度,一個基於字體和顯示文本的首選寬度。UIProgressView 僅有一個基於其插圖的首選高度,但沒有首選寬度。一個沒有格式的 UIView 既沒有首選寬度也沒有首選高度。

你須要根據想要顯示的內容來決定你的自定義視圖是否具備一個固有內容尺寸,若是有的話,它是在哪一個尺度上固有。

爲了在自定義視圖中實現固有內容尺寸,你須要作兩件事:重寫 intrinsicContentSize 爲內容返回恰當的大小,不管什麼時候有任何會影響固有內容尺寸的改變發生時,調用 invalidateIntrinsicContentSize。若是這個視圖只有一個方向的尺寸設置了固有尺寸,那麼爲另外一個方向的尺寸返回 UIViewNoIntrinsicMetric / NSViewNoIntrinsicMetric

須要注意的是,固有內容尺寸必須是獨立於視圖 frame 的。例如,不可能返回一個基於 frame 高度或寬度的特定高寬比的固有內容尺寸。

壓縮阻力 (Compression Resistance) 和 內容吸附 (Content Hugging)

譯者注 我理解爲壓縮阻力和內容吸附性,實在是想不到更貼切的名稱了。壓縮阻力是控制視圖在兩個方向上的收縮性,內容吸附性是當視圖的大小改變時,它會盡可能讓視圖靠近它的固有內容尺寸

每一個視圖在兩個方向上都分配有內容壓縮阻力優先級和內容吸附性優先級。只有當視圖定義了固有內容尺寸時這些屬性才能起做用,若是沒有定義內容大小,那就無法阻止被壓縮或者吸附了。

在後臺中,固有內容尺寸和這些優先值被轉換爲約束條件。一個固有內容尺寸爲 {100,30} 的 label,水平/垂直壓縮阻力優先值爲 750,水平/垂直的內容吸附性優先值爲 250,這四個約束條件將會生成:

H:[label(<=100@250)]
H:[label(>=100@750)]
V:[label(<=30@250)]
V:[label(>=30@750)]

複製代碼

若是你不熟悉上面約束條件所使用的可視格式語言,你能夠到 Apple 文檔 中瞭解。記住,這些額外的約束條件對了解自動佈局的行爲產生了隱式的幫助,同時也更好理解它的錯誤信息。

Frame 和 Alignment Rect

自動佈局並不會操做視圖的 frame,但能做用於視圖的 alignment rect。你們很容易忘記它們之間細微的差異,由於在不少狀況下,它們是相同的。可是alignment rect 其實是一個強大的新概念:從一個視圖的視覺外觀解耦出視圖的 layout alignment edges。

好比,一個自定義 icon 類型的按鈕比咱們指望點擊目標還要小的時候,這將會很難佈局。當插圖顯示在一個更大的 frame 中時,咱們將不得不瞭解它顯示的大小,而且相應調整按鈕的 frame,這樣 icon 纔會和其餘界面元素排列好。當咱們想要在內容的周圍繪製像 badges,陰影,倒影的裝飾時,也會發生一樣的狀況。

咱們可使用 alignment rect 簡單的定義須要用來佈局的矩形。在大多數狀況下,你僅須要重寫 alignmentRectInsets 方法,這個方法容許你返回相對於 frame 的 edge insets。若是你須要更多控制權,你能夠重寫 alignmentRectForFrame: 和 frameForAlignmentRect:。若是你不想減去固定的 insets,而是計算基於當前 frame 的 alignment rect,那麼這兩個方法將會很是有用。可是你須要確保這兩個方法是互爲可逆的。

關於這點,回憶上面說起到的視圖固有內容尺寸引用它的 alignment rect,而不是 frame。這是有道理的,由於自動佈局直接根據固有內容尺寸產生壓縮阻力和內容吸附約束條件。

基線對齊 (Baseline Alignment)

爲了讓使用 NSLayoutAttributeBaseline 屬性的約束條件對自定義視圖奏效,咱們須要作一些額外的工做。固然,這隻有咱們討論的自定義視圖中有相似基準線的東西時,纔有意義。

在 iOS 中,能夠經過實現 viewForBaselineLayout 來激活基線對齊。在這裏返回的視圖底邊緣將會做爲 基線。默認實現只是簡單的返回本身,然而自定義的實現能夠返回任何子視圖。在 OS X 中,你不須要返回一個子視圖,而是從新定義 baselineOffsetFromBottom 返回一個從視圖底部邊緣開始的 offset,這和在 iOS 中同樣,默認實現都是返回 0。

控制佈局

在自定義視圖中,你能徹底控制它子視圖的佈局。你能夠增長本地約束;根據內容變化須要,你能夠改變本地約束;你能夠爲子視圖調整佈局操做的結果;或者你能夠選擇拋棄自動佈局。

但確保你明智的使用這個權利。大多數狀況下能夠簡單地經過爲你的子視圖簡單的增長本地約束來處理。

本地約束

若是咱們想用幾個子視圖組成一個自定義視圖,咱們須要以某種方式佈局這些子視圖。在自動佈局的環境中,天然會想到爲這些視圖增長本地約束。然而,須要注意的是,這將會使你自定義的視圖是基於自動佈局的,這個視圖不能再被使用於未啓用自動佈局的 windows 中。最好經過實現 requiresConstraintBasedLayout 返回 YES 明確這個依賴。

添加本地約束的地方是 updateConstraints。確保在你的實現中增長任何你須要佈局子視圖的約束條件以後,調用一下 [super updateConstraints]。在這個方法中,你不會被容許禁用何約束條件,由於你已經進入上面所描述的佈局過程的第一步了。若是嘗試着這樣作,將會產生一個友好的錯誤信息 「programming error」。

若是稍後一個失效的約束條件發生了改變的話,你須要馬上移除這個約束並調用 setNeedsUpdateConstraints。事實上,僅在這種狀況下你須要觸發更新約束條件的操做。

控制子視圖佈局

若是你不能利用佈局約束條件達到子視圖預期的佈局,你能夠進一步在 iOS 裏重寫 layoutSubviews 或者在 OS X 裏面重寫 layout。經過這種方式,當約束條件系統獲得解決而且結果將要被應用到視圖中時,你便已經進入到佈局過程的第二步。

最極端的狀況是不調用父類的實現,本身重寫所有的 layoutSubviews / layout。這就意味着你在這個視圖裏的視圖樹裏拋棄了自動佈局。從如今起,你能夠按喜歡的方式手動放置子視圖。

若是你仍然想使用約束條件佈局子視圖,你須要調用 [super layoutSubviews] / [super layout],而後對佈局進行微調。你能夠經過這種方式建立那些經過定於約束沒法實現的布,好比,由到視圖大小之間的關係或是視圖之間間距的關係來定義的佈局。

這方面另外一個有趣的使用案例就是建立一個佈局依賴的視圖樹。當自動佈局完成第一次傳遞而且爲自定義視圖的子視圖設置好 frame 後,你即可以檢查子視圖的位置和大小,併爲視圖層級和(或)約束條件作出調整。WWDC session 228 – Best Practices for Mastering Auto Layout 有一個很好的例子。

你也能夠在第一次佈局操做完成後再決定改變約束條件。好比,若是視圖變得太窄的話,將原來排成一行的子視圖轉變成兩行。

- layoutSubviews
{
    [super layoutSubviews];
    if (self.subviews[0].frame.size.width <= MINIMUM_WIDTH)
    {
        [self removeSubviewConstraints];
        self.layoutRows += 1; [super layoutSubviews];
    }
}

- updateConstraints
{
    // 根據 self.layoutRows 添加約束...
    [super updateConstraints];
}

複製代碼

多行文本的固有內容尺寸

UILabel 和 NSTextField 對於多行文本的固有內容尺寸是模糊不清的。文本的高度取決於行的寬度,這也是解決約束條件時須要弄清的問題。爲了解決這個問題,這兩個類都有一個叫作 preferredMaxLayoutWidth 的新屬性,這個屬性指定了行寬度的最大值,以便計算固有內容尺寸。

由於咱們一般不能提早知道這個值,爲了得到正確的值咱們須要先作兩步操做。首先,咱們讓自動佈局作它的工做,而後用佈局操做結果的 frame 更新給首選最大寬度,而且再次觸發佈局。

- (void)layoutSubviews
{
    [super layoutSubviews];
    myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
    [super layoutSubviews];
}

複製代碼

第一次調用 [super layoutSubviews] 是爲了得到 label 的 frame,而第二次調用是爲了改變後更新佈局。若是省略第二個調用咱們將會獲得一個 NSInternalInconsistencyException 的錯誤,由於咱們改變了更新約束條件的佈局操做,但咱們並無再次觸發佈局。

咱們也能夠在 label 子類自己中這樣作:

@implementation MyLabel
- (void)layoutSubviews
{
    self.preferredMaxLayoutWidth = self.frame.size.width;
    [super layoutSubviews];
}
@end

複製代碼

在這種狀況下,咱們不須要先調用 [super layoutSubviews],由於當 layoutSubviews 被調用時,label 就已經有一個 frame 了。

爲了在視圖控制器層級作出這樣的調整,咱們用掛鉤到 viewDidLayoutSubviews。這時候第一個自動佈局操做的 frame 已經被設置,咱們能夠用它們來設置首選最大寬度。

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
    [self.view layoutIfNeeded];
}

複製代碼

最後,確保你沒有給 label 設置一個比 label 內容壓縮阻力優先級還要高的具體高度約束。不然它將會取代根據內容計算出的高度。

動畫

說到根據自動佈局的視圖動畫,有兩個不一樣的基本策略:約束條件自身動態化;以及改變約束條件從新計算 frame,並使用 Core Animation 將 frame 插入到新舊位置之間。

這兩種處理方法不一樣的是:約束條件自身動態化產生的佈局結果老是符合約束條件系統。與此相反,使用 Core Animation 插入值到新舊 frame 之間會臨時違反約束條件。

直接使用約束條件動態化只是在 OS X 上的一種可行策略,而且這對你能使用的動畫有侷限性,由於約束條件一旦建立後,只有其常量能夠被改變。在 OS X 中你能夠在約束條件的常量中使用動畫代理來驅動動畫,而在 iOS 中,你只能手動進行控制。另外,這種方法明顯比 Core Animation 方法慢得多,這也使得它暫時不適合移動平臺。

當使用 Core Animation 方法時,即便不使用自動佈局,動畫的工做方式在概念上也是同樣的。不一樣的是,你不須要手動設置視圖的目標 frames,取而代之的是修改約束條件並觸發一個佈局操做爲你設置 frames。在 iOS 中,代替:

[UIView animateWithDuration:1 animations:^{
    myView.frame = newFrame;
}];

複製代碼

你如今須要寫:

// 更新約束
[UIView animateWithDuration:1 animations:^{
    [myView layoutIfNeeded];
}];

複製代碼

請注意,使用這種方法,你能夠對約束條件作出的改變並不侷限於約束條件的常量。你能夠刪除約束條件,增長約束條件,甚至使用臨時動畫約束條件。因爲新的約束只被解釋一次來決定新的 frames,因此更復雜的佈局改變都是有可能的。

須要記住的是:Core Animation 和 Auto Layout 結合在一塊兒產生視圖動畫時,本身不要接觸視圖的 frame。一旦視圖使用自動佈局,那麼你已經將設置 frame 的責任交給了佈局系統。你的干擾將形成怪異的行爲。

這也意味着,若是使用的視圖變換 (transform) 改變了視圖的 frame 的話,它和自動佈局是沒法一塊兒正常使用的。考慮下面這個例子:

[UIView animateWithDuration:1 animations:^{
    myView.transform = CGAffineTransformMakeScale(.5, .5);
}];

複製代碼

一般咱們指望這個方法在保持視圖的中心時,將它的大小縮小到原來的一半。可是自動佈局的行爲是根據咱們創建的約束條件種類來放置視圖的。若是咱們將其居中於它的父視圖,結果便像咱們預想的同樣,由於應用視圖變換會觸發一個在父視圖內居中新 frame 的佈局操做。然而,若是咱們將視圖的左邊緣對齊到另外一個視圖,那麼這個 alignment 將會粘連住,而且中心點將會移動。

無論怎麼樣,即便最初的結果跟咱們預想的同樣,像這樣經過約束條件將轉換應用到視圖佈局上並非一個好主意。視圖的 frame 沒有和約束條件同步,也將致使怪異的行爲。

若是你想使用 transform 來產生視圖動畫或者直接使它的 frame 動態化,最乾淨利索的技術是將這個視圖嵌入到一個視圖容器內,而後你能夠在容器內重寫 layoutSubviews,要麼選擇徹底脫離自動佈局,要麼僅僅調整它的結果。舉個例子,若是咱們在咱們的容器內創建一個子視圖,它根據容器的頂部和左邊緣自動佈局,當佈局根據以上的設置縮放轉換後咱們能夠調整它的中心:

- (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 裏面的容器,而且使用約束條件放置它的的子視圖,同時還可以根據固定的中心應用縮放轉換。

調試

當談到調試自動佈局,OS X 比 iOS 還有一個重要的優點。在 OS X 中,你能夠利用 Instrument 的 Cocoa Layout 模板,或者是 NSWindow 的 visualizeConstraints: 方法。並且 NSView 有一個 identifier 屬性,爲了得到更多可讀的自動佈局錯誤信息,你能夠在 Interface Builder 或代碼裏面設置這個屬性。

不可知足的約束條件

若是咱們在 iOS 中遇到不可知足的約束條件,咱們只能在輸出的日誌中看到視圖的內存地址。尤爲是在更復雜的佈局中,有時很難辨別出視圖的哪一部分出了問題。然而,在這種狀況下,還有幾種方法能夠幫到咱們。

首先,當你在不可知足的約束條件錯誤信息中看到 NSLayoutResizingMaskConstraints 時,你確定忘了爲你某一個視圖設定 translatesAutoResizingMaskIntoConstraints 爲 NO。Interface Builder 中會自動設置,可是使用代碼時,你須要爲全部的視圖手動設置。

若是不是很明確是哪一個視圖致使的問題,你就須要經過內存地址來辨認視圖。最簡單的方法是使用調試控制檯。你能夠打印視圖自己或它父視圖的描述,甚至遞歸描述的樹視圖。這一般會提示你須要處理哪一個視圖。

(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>>

複製代碼

一個更直觀的方法是在控制檯修改有問題的視圖,這樣你能夠在屏幕上標註出來。好比,你能夠改變它的背景顏色:

(lldb) expr ((UIView *)0x7731880).backgroundColor = [UIColor purpleColor]

複製代碼

確保從新執行你的程序,不然改變不會在屏幕上顯示出來。還要注意將內存地址轉換爲 (UIView *) ,以及額外的圓括號,這樣咱們就可使用點操做。另外,你固然也能夠經過發送消息來實現:

(lldb) expr [(UIView *)0x7731880 setBackgroundColor:[UIColor purpleColor]]

複製代碼

另外一種方法是使用 Instrument 的 allocation 模板,根據圖表分析。一旦你從錯誤消息中獲得內存地址(運行 Instruments 時,你從 Console 應用中得到的錯誤消息),你能夠將 Instrument 的詳細視圖切換到 Objects List 頁面,而且用 Cmd-F 搜索那個內存地址。這將會爲你顯示分配視圖對象的方法,這一般是一個很好的暗示(至少對那些由代碼建立的視圖來講是這樣的)。

你也能夠經過改進錯誤信息自己,來更容易地在 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

複製代碼

若是整數的 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

複製代碼

經過這種方法錯誤消息變得更可讀,而且你不須要找出內存地址對應的視圖。然而,對你而言,你須要作一些額外的工做以確保每次爲視圖分配的名字都是有意義。

Daniel 提出了另外一個很巧妙的方法,能夠爲你提供更好的錯誤消息而且不須要額外的工做:對於每一個佈局約束條件,都須要將調用棧的標誌融入到錯誤消息中。這樣就很容易看出來問題涉及到的約束了。要作到這一點,你須要 swizzle UIView 或者 NSView 的 addConstraint: / addConstraints: 方法,以及佈局約束的 description 方法。在添加約束的方法中,你須要爲每一個約束條件關聯一個對象,這個對象描述了當前調用棧堆棧的第一個棧頂信息(或者任何你從中獲得的信息):

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倉庫,瞭解這一技術的代碼示例。

有歧義的佈局

另外一個常見的問題就是有歧義的佈局。若是咱們忘記添加一個約束條件,咱們常常會想爲何佈局看起來不像咱們所指望的那樣。UIView 和 NSView 提供三種方式來查明有歧義的佈局:hasAmbiguousLayoutexerciseAmbiguityInLayout,和私有方法 _autolayoutTrace

顧名思義,若是視圖存在有歧義的佈局,那麼 hasAmbiguousLayout 返回YES。若是咱們不想本身遍歷視圖層並記錄這個值,可使用私有方法 _autolayoutTrace。這將返回一個描述整個視圖樹的字符串:相似於 recursiveDescription 的輸出(當視圖存在有歧義的佈局時,這個方法會告訴你)。

因爲這個方法是私有的,確保正式產品裏面不要包含調用這個方法的任何代碼。爲了防止你犯這種錯誤,你能夠在視圖的category中這樣作:

@implementation UIView (AutoLayoutDebugging)
- (void)printAutoLayoutTrace {
    #ifdef DEBUG
    NSLog(@"%@", [self performSelector:@selector(_autolayoutTrace)]);
    #endif
}
@end

複製代碼

_autolayoutTrace 打印的結果以下:

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>

複製代碼

正如不可知足約束條件的錯誤消息同樣,咱們仍然須要弄明白打印出的內存地址所對應的視圖。

另外一個標識出有歧義佈局更直觀的方法就是使用 exerciseAmbiguityInLayout。這將會在有效值之間隨機改變視圖的 frame。然而,每次調用這個方法只會改變 frame 一次。因此當你啓動程序的時候,你根本不會看到改變。建立一個遍歷全部視圖層級的輔助方法是一個不錯的主意,而且讓全部包含歧義佈局的視圖「晃動 (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選項

有幾個有用的 NSUserDefault 選項能夠幫助咱們調試、測試自動佈局。你能夠在代碼中設定,或者你也能夠在 scheme editor 中指定它們做爲啓動參數。

顧名思義,UIViewShowAlignmentRects和 NSViewShowAlignmentRects 設置視圖可見的 alignment rects。NSDoubleLocalizedStrings 簡單的獲取並複製每一個本地化的字符串。這是一個測試更長語言佈局的好方法。最後,設置 AppleTextDirection 和 NSForceRightToLeftWritingDirection 爲 YES,來模擬從右到左的語言。

編者注 若是你不知道怎麼在 scheme 中設置相似 NSDoubleLocalizedStrings,這裏有一張圖來講明;

pic

約束條件代碼

當在代碼中設置視圖和它們的約束條件時候,必定要記得將 translatesAutoResizingMaskIntoConstraints 設置爲 NO。若是忘記設置這個屬性幾乎確定會致使不可知足的約束條件錯誤。即便你已經用自動佈局一段時間了,但仍是要當心這個問題,由於很容易在不經意間發生產生這個錯誤。

當你使用 可視化結構語言 (visual format language, VFL) 設置約束條件時, constraintsWithVisualFormat:options:metrics:views: 方法有一個頗有用的 option 參數。若是你尚未用過,請參見文檔。這不一樣于格式化字符串只能影響一個視圖,它容許你調整在必定範圍內的視圖。舉個例子,若是用可視格式語言指定水平佈局,那麼你可使用 NSLayoutFormatAlignAllTop 排列可視語言裏全部視圖爲上邊緣對齊。

還有一個使用可視格式語言在父視圖中居中子視圖的小技巧,這技巧利用了不均等約束和可選參數。下面的代碼在父視圖中水平排列了一個視圖:

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 選項在父視圖和子視圖間建立了居中約束。格式化字符串自己只是一個虛擬的東西,它會產生一個指定的約束,一般狀況下只要子視圖是可見的,那麼父視圖底部和子視圖頂部邊緣之間的空間就應該小於等於1點。你能夠顛倒示例中的方向達到垂直居中的效果。

使用可視格式語言另外一個方便的輔助方法就是咱們在上面例子中已經使用過的 NSDictionaryFromVariableBindings 宏指令,你傳遞一個可變數量的變量過去,返回獲得一個鍵爲變量名的字典。

爲了佈局任務,你須要一遍一遍的調試,你能夠方便的建立本身的輔助方法。好比,你想要垂直地排列一系列視圖,想要它們垂直方向間距一致,水平方向上全部視圖以它們的左邊緣對齊,用下面的方法將會方便不少:

@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

複製代碼

同時也有許多不一樣的自動佈局的庫採用了不一樣的方法來簡化約束條件代碼。

性能

自動佈局是佈局過程當中額外的一個步驟。它須要一組約束條件,並把這些約束條件轉換成 frame。所以這天然會產生一些性能的影響。你須要知道的是,在絕大數狀況下,用來解決約束條件系統的時間是能夠忽略不計的。可是若是你正好在處理一些性能關鍵的視圖代碼時,最好仍是對這一點有所瞭解。

例如,有一個 collection view,當新出現一行時,你須要在屏幕上呈現幾個新的 cell,而且每一個 cell 包含幾個基於自動佈局的子視圖,這時你須要注意你的性能了。幸運的是,咱們不須要用直覺來感覺上下滾動的性能。啓動 Instruments 真實的測量一下自動佈局消耗的時間。小心 NSISEngine 類的方法。

另外一種狀況就是當你一次顯示大量視圖時可能會有性能問題。將約束條件轉換成視圖的 frame 時,用來計算約束的算法超線性複雜的。這意味着當有必定數量的視圖時,性能將會變得很是低下。而這確切的數目取決於你具體使用狀況和視圖配置。可是,給你一個粗略的概念,在當前 iOS 設備下,這個數字大概是 100。你能夠讀這兩個博客帖子瞭解更多的細節。

記住,這些都是極端的狀況,不要過早的優化,而且避免自動佈局潛在的性能影響。這樣大多數狀況便不會有問題。可是若是你懷疑這花費了你決定性的幾十毫秒,從而致使用戶界面不徹底流暢的話,分析你的代碼,而後你再去考慮用回手動設置 frame 有沒有意義。此外,硬件將會變得愈來愈能幹,而且Apple也會繼續調整自動佈局的性能。因此現實世界中極端狀況的性能問題也將隨着時間減小。

結論

自動佈局是一個建立靈活用戶界面的強大功能,這種技術不會消失。剛開始使用自動佈局時可能會有點困難,但總會有柳暗花明的一天。一旦你掌握了這種技術,而且掌握了排錯的小技巧,即可庖丁解牛,恍然大悟:這太符合邏輯了。


原文 Advanced Auto Layout Toolbox

相關文章
相關標籤/搜索