深入理解先進的自動佈局工具箱

原文地址:先進的自動佈局工具箱

 
在我的上一個項目中,因爲是面向公司內部使用的客戶端,所以我直接拋棄了iOS5,在項目中大量使用了iOS6中的新特性:自動佈局,才發現生活可以如此美好(除了調bug的時候),發張圖大家感受一下,下面分別爲橫屏和豎屏下的佈局,再也不要像之前那樣適配的死去活來了:horizontalvertical
 

 
這篇文章並沒有具體介紹自動佈局的一些基本概念,主要講解了一些高級的使用方法和調試技巧,文中有的句子比較長,意思也有點難懂,所以需要靜下心仔細揣摩。如果你剛接觸自動佈局,推薦你先看這幾篇文章:1.官方的Guide:About Cocoa Auto Layout  2.來自raywenderlich的tutorial:Beginning Auto Layout in iOS 6: Part 2/2    3.來自@onevcat 的blog:WWDC 2012 Session筆記——202, 228, 232 AutoLayout(自動佈局)入門
 
##############################以下是正文###########################
 
自動佈局在OS X10.7中被引進,一年後在iOS 6中也可以用了。不久在iOS 7中的程序將會有望設置全局字體大小,因此,幾乎在不同屏幕大小和方向上,用戶界面佈局需要更大的靈活性。Apple也在自動佈局上花了很大功夫,所以如果你還沒做過這一塊,現在就是接觸這個技術的好時機。
 
很多開發者在第一次嘗試使用時都非常掙扎,因爲用Xcode 4的Interface Builder建立基於佈局約束的體驗非常糟糕。但不要因爲這個灰心。自動佈局其實比現在Interface Builder所支持的要好。Xcode 5在這塊中將會帶來重要的變化。
 
這篇文章不是用來介紹Auto Layout的。如果你還沒用過它,那還是先去WWDC 2012看看基礎教程吧(1,2,3)。
 
反而我們會專注於一些高級的技巧和方法,這將會讓你使用自動佈局的時候效率更高,生活更幸福。大多數內容在WWDC會議中都有提到,但是他們都是在日常工作中容易被監督或遺忘的。
 
佈局過程
首先我們總結一下自動佈局將視圖顯示到屏幕上的步驟。當你根據自動佈局盡力寫出你想要的佈局種類時,特別是高級的使用情況和動畫,這有利於後退一步,並回憶佈局過程是怎麼工作的。
 
和springs,struts比起來,在視圖被顯示之前,自動佈局引入了兩個額外的步驟:更新約束和佈局視圖。每一步都是基於前一步操作的;顯示基於佈局視圖,佈局視圖基於更新約束。
 
第一步:更新約束,可以被認爲是一個“計量傳遞”。這發生於自下而上(從子視圖到父視圖),並準備設置視圖frame所需要的佈局信息。你可以通過調用setNeedsUpdateConstraints來觸發這個傳遞,同時,你對約束條件系統做出的任何改變都將自動觸發這個方法。無論如何,通知自動佈局關於自定義視圖中任何可能影響佈局的改變是非常有用的。談到自定義視圖,你可以在這個階段重寫updateConstraints來爲你的視圖增加需要的本地約束。
 
第二步:佈局,發生於自上而下(從父視圖到子視圖)。這種佈局傳遞實際上是通過設置frame(在OS X中)或者center和bounds(在iOS中)將約束條件系統的解決方案應用到視圖上。你可以通過調用setNeedsLayout來觸發這個傳遞,這並不會立刻應用佈局,而是注意你稍後的請求。因爲所有的佈局請求將會被合併到一個佈局傳遞,所以你不需要爲經常調用這個方法而困擾。
 
你可以調用layoutIfNeeded/layoutSubtreeIfNeeded(iOS/OS X)來強制系統立即更新視圖樹的佈局。如果你下一步操作依賴於更新後視圖的frame,這將非常有用。在你自定義的視圖中,你可以重寫layoutSubviews/layout來獲得控制佈局變化的所有權。我們稍後將展示使用方法。
 
最終,不管你是否用了自動佈局,顯示器都會將自上而下將渲染視圖傳遞到屏幕上,你也可以通過調用setNeedsDisplay來觸發,這將會導致所有的調用都被合併到一起推遲重繪。重寫熟悉的drawRect:能夠讓我們獲得自定視圖中顯示過程的所有權。 既然每一步都是基於前一步操作的,如果有任何佈局改變沒有被解決,那麼,顯示傳遞將會觸發一個佈局傳遞。相同的,如果約束條件系統有沒有更新的改變,佈局變化也將會觸發更新約束條件。
 
需要牢記的是,這三步並不是單向的。基於約束條件的佈局是一個迭代的過程,佈局傳遞可以基於前一個佈局方案做出更改,這將再次接着另一個佈局傳遞後觸發更新約束條件。這可以被用來創建高級的自定義視圖佈局,但是如果你每一次調用自定義layoutSubviews都會導致另一個佈局傳遞,那麼你將會陷入一個無限循環中。
 
爲自定義視圖激活自動佈局
當創建一個自定義視圖時,你需要知道關於自動佈局的這些事情:具體指定一個合適的固有內容大小,區分開視圖的frame和alignment rect,激活baseline-aligned佈局,如何hook into到佈局過程。我們將會逐一瞭解這些部分。
 
固有內容大小(Intrinsic Content Size )
固有內容大小是一個視圖期望爲其顯示特定內容得到的大小。比如,UILabel有一個基於字體的首選高度,一個基於字體和顯示文本的首選寬度。一個UIProgressView僅有一個基於其插圖的首選高度,但沒有首選寬度。一個沒有格式的UIView既沒有首選寬度也沒有首選高度。
 
如果你自定義的視圖有一個固有內容大小,你必須決定,根據內容來顯示,而且你需要指定這個大小。
 
爲了在自定義視圖中實現固有內容大小,你需要做兩件事:重寫 intrinsicContentSize爲內容返回恰當的大小,無論何時有任何會影響固有內容大小的改變發生時,調用invalidateIntrinsicContentSize。如果這個視圖只有一個方向的尺寸設置了固有大小,那麼爲另一個方向的尺寸返回UIViewNoIntrinsicMetric/NSViewNoIntrinsicMetric。 需要注意的是,固有內容大小必須是獨立於視圖frame的。例如,不可能返回一個基於frame特定高寬比的固有內容大小。
 
Compression Resistance and 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實際上是一個強大的新概念:從一個視圖可視外觀分離出佈局對齊邊緣。
 
比如,一個自定義icon類型的按鈕比我們期望點擊目標還要小的時候,這將會很難佈局。當插圖顯示在一個更大的frame中時,我們將不得不瞭解它顯示的大小,並且調整相應按鈕的frame,這樣icon纔會和其他界面元素排列好。當我們想要在內容的周圍繪製像badges,陰影,倒影的裝飾時,也會發生同樣的情況。
 
我們可以使用alignment rect簡單的定義需要用來佈局的矩形。在大多數情況下,你僅需要重寫alignmentRectInsets方法,這個方法允許你返回相對於frame的edge insets。如果你需要更多控制權,你可以重寫alignmentRectForFrame:和frameForAlignmentRect:。如果你不想減去固定的insets,而是計算基於當前frame的alignment rect,那麼這兩個方法將會非常有用。但是你需要確保這兩個方法是互爲可逆的。
 
在這種情況下,回憶上面提及到的視圖固有內容大小引用它的alignment rect,而不是frame。這是有道理的,因爲自動佈局直接根據固有內容大小產生壓縮阻力和內容吸附約束條件。
 
Baseline Alignment
爲了使約束條件能夠使用NSLayoutAttributeBaseline屬性對自定義視圖奏效,我們需要做一些額外的工作。當然,只有我們討論的自定義視圖中有類似baseline的東西時,才起作用。
 
在iOS中,可以通過實現viewForBaselineLayout來激活baseline alignment。在這裏返回的視圖底邊緣將會作爲baseline。默認實現只是簡單的返回自己,然而自定義的實現可以返回任何子視圖。在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有一個很好的例子。 你也可以在第一次佈局傳遞完成後再決定改變約束條件。比如,如果視圖變得太窄,將排成一行的子視圖轉變成兩行。

 

  1. - layoutSubviews 
  2.     [super layoutSubviews]; 
  3.     if (self.subviews[0].frame.size.width &lt;= MINIMUM_WIDTH) 
  4.     { 
  5.         [self removeSubviewConstraints]; 
  6.         self.layoutRows += 1; [super layoutSubviews]; 
  7.     } 
  8.   
  9. - updateConstraints 
  10.     [super updateConstraints]; // add constraints depended on self.layoutRows... 
 
多行文本的固有內容大小
 
UILabel和NSTextField對於多行文本的固有內容大小是模糊不清的。文本的高度取決於線的寬度,這也是解決約束條件時需要弄清的問題。爲了解決這個問題,這兩個類都有一個叫做preferredMaxLayoutWidth的新屬性,這個屬性指定了線寬度的最大值,以便計算固有內容大小。
 
因爲我們通常不能提前知道這個值,爲了獲得正確的值我們需要先做兩步操作。首先,我們讓自動佈局做它的工作,然後用佈局傳遞結果的frame更新給首選最大寬度,並且再次觸發佈局。
  1. - (void)layoutSubviews 
  2.     [super layoutSubviews]; 
  3.   
  4.     myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width; 
  5.   
  6.     [super layoutSubviews]; 
  7.   
 
第一次調用[super layoutSubviews]是爲了獲得label的frame,而第二次調用是改變後更新佈局。如果省略第二個調用我們將會得到一個NSInternalInconsistencyException的錯誤,因爲我們改變了更新約束條件的佈局傳遞,但我們並沒有再次觸發佈局。
 
我們也可以在label子類本身中這樣做:
  1. @implementation MyLabel 
  2.   
  3. - (void)layoutSubviews 
  4.     self.preferredMaxLayoutWidth = self.frame.size.width; 
  5.     [super layoutSubviews]; 
  6. @end 
 
在這種情況下,我們不需要先調用[super layoutSubviews],因爲當layoutSubviews被調用時,label就已經有一個frame了。
 
爲了在視圖控制器層級做出這樣的調整,我們用進入到viewDidLayoutSubviews。這時候第一個自動佈局傳遞的frame已經被設置,我們可以用他們來設置首選最大寬度。
  1. - (void)viewDidLayoutSubviews 
  2.     [super viewDidLayoutSubviews]; 
  3.     myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width; 
  4.     [self.view layoutIfNeeded]; 
最後,確保你沒有給label設置一個比label內容壓縮阻力優先級還要高的具體高度約束。否則它將會取代根據內容計算出的高度。
 
動畫
說到根據自動佈局的視圖動畫,有兩個不同的基本策略:約束條件自身動態化;改變約束條件重新計算frame,並使用Core Animation將frame插入到新舊位置之間。
 
這兩種處理方法不同的是:約束條件自身動態化產生的佈局結果總是符合約束條件系統。與此同時,使用Core Animation插入值到新舊frame之間會臨時違反約束條件。
 
直接使用約束條件動態化只是在OS X上的一種可行策略,並且這對你能使用的動畫有侷限性,因爲一旦創建後,約束條件只有一個常量可以被改變。在iOS中,你只好手動控制動畫了,然而在OS X中你可以在約束條件的常量中使用動畫代理。而且,這種方法明顯比Core Animation方法慢,這也使得它暫時不適合移動平臺。
 
當使用Core Animation方法時,即使不使用自動佈局,動畫的工作方式在概念上是一樣的。不同的是,你不需要手動設置視圖的目標frames,取而代之的是修改約束條件並觸發一個佈局傳遞爲你設置frames。在iOS中,代替:
  1. [UIView animateWithDuration:1 animations:^{ 
  2.     myView.frame = newFrame; 
  3. }]; 
 
你現在需要寫:
  1. // update constraints 
  2. [UIView animateWithDuration:1 animations:^{ 
  3.     [myView layoutIfNeeded]; 
  4. }]; 
 
請注意,使用這種方法,你可以對約束條件做出的改變並不侷限於約束條件的常量。你可以刪除約束條件,增加約束條件,甚至使用臨時動畫約束條件。由於新的約束只被解釋一次來決定新的frames,所以更復雜的佈局改變都是有可能的。
 
需要記住的是:Core Animation和Auto Layout結合在一起產生視圖動畫時,自己不要接觸視圖的frame。一旦視圖使用自動佈局,那麼你已經將設置frame的責任交給了佈局系統。你的干擾將造成怪異的行爲。
 
這也意味着,如果自動佈局改變視圖的frame,使用自動佈局的視圖變換也不一定總是運行良好的。考慮下面這個例子:
  1. [UIView animateWithDuration:1 animations:^{ 
  2.     myView.transform = CGAffineTransformMakeScale(.5, .5); 
  3. }]; 
 
通常我們期望這個方法在在保持視圖的中心時,將它的大小縮小到原來的一半。但是自動佈局的行爲是根據我們建立的約束條件種類來放置視圖。如果我們將其居中於它的父視圖,結果便像我們預想的一樣,因爲應用視圖變換會觸發一個在父視圖內居中新frame的佈局傳遞。然而,如果我們將視圖的左邊緣對齊到另一個視圖,那麼這個alignment將會粘連住,並且中心點將會移動。
 
不管怎麼樣,即使最初的結果跟我們預想的一樣,像這樣通過約束條件將轉換應用到視圖佈局上並不是一個好主意。視圖的frame沒有和約束條件同步,也將導致怪異的行爲。
 
如果你想使用轉換來產生視圖動畫或者直接使他的frame動態化,最乾淨利索的技術是將這個視圖嵌入到一個視圖容器內,然後你可以在容器內重寫layoutSubviews,要麼選擇完全脫離自動佈局,要麼僅僅調整他的結果。舉個例子,如果我們在我們的容器內建立一個子視圖,它根據容器的頂部和左邊緣自動佈局,當佈局根據以上的設置縮放轉換後我們可以調整它的中心:
  1. - (void)layoutSubviews 
  2.     [super layoutSubviews]; 
  3.     static CGPoint center = {0,0}; 
  4.     if (CGPointEqualToPoint(center, CGPointZero)) { 
  5.         // grab the view's center point after initial layout 
  6.         center = self.animatedView.center; 
  7.     } else { 
  8.         // apply the previous center to the animated view 
  9.         self.animatedView.center = center; 
  10.     } 
如果我們將animatedView屬性暴露爲IBOutlet,我們甚至可以使用Interface Builder裏面的容器,並且使用約束條件放置它的的子視圖,同時還能夠根據固定的中心應用縮放轉換。
 
調試
當談到調試自動佈局,OS X比iOS還有一個重要的優勢。在OS X中,和NSWindow的visualizeConstraints:方法一樣,你可以利用Instrument的Cocoa Layout模板。而且,NSView有一個identifier屬性,爲了獲得更多可讀的自動佈局錯誤信息,你可以在Interface Builder或代碼裏面設置這個屬性。
 
不可滿足的約束條件
如果我們在iOS中遇到不可滿足的約束條件,我們只能在輸出的日誌中看到視圖的內存地址。尤其是在更復雜的佈局中,有時很難辨別出視圖的哪一部分出了問題。然而,在這種情況下,還有幾種方法可以幫到我們。
 
首先,當你在不可滿足的約束條件錯誤信息中看到NSLayoutResizingMaskConstraints時,你肯定忘了爲你某一個視圖設定translatesAutoResizingMaskIntoConstraints爲NO。Interface Builder中會自動設置,但是使用代碼時,你需要爲所有的視圖手動設置。
 
如果不是很明確那個視圖計算問題,你需要通過內存地址來辨認視圖。最簡單的方法是使用調試控制檯。你可以打印視圖本身或它父視圖的描述,甚至遞歸描述的樹視圖。這通常會提示你需要處理哪個視圖。
 
一個更直觀的方法是在控制檯修改有問題的視圖,這樣你可以在屏幕上標註出來。比如,你可以改變它的背景顏色:
  1. (lldb) expr ((UIView *)0x7731880).backgroundColor = [UIColor purpleColor] 
 
確保重新執行程序後改變不會在屏幕上顯示出來。還要注意將內存地址轉換爲(UIView *),以及額外的圓括號,這樣我們就可以使用點操作。另外,你當然也可以通過發送消息:
  1. (lldb) expr [(UIView *)0x7731880 setBackgroundColor:[UIColor purpleColor]] 
 
另一種方法是使用Instrument的allocation模板,根據圖表分析。一旦你從錯誤消息中得到內存地址(運行Instruments時,你從控制檯中獲得的錯誤消息),你可以將Instrument切換到Objects List的詳細視圖,並且用Cmd-F搜索那個內存地址。這將會爲你顯示分配視圖對象的方法,這通常是一個很好的暗示(至少找到創建視圖對象的代碼了)。
 
你也可以在iOS中弄懂不可滿足的約束條件錯誤,這比改善錯誤消息來的更簡單。我們可以在一個category中重寫NSLayoutConstraint的描述,並且將視圖的tags包含進去:
  1. @implementation NSLayoutConstraint (AutoLayoutDebugging) 
  2.   
  3. #ifdef DEBUG 
  4.   
  5. - (NSString *)description 
  6.     NSString *description = super.description; 
  7.     NSString *asciiArtDescription = self.asciiArtDescription; 
  8.     return [description stringByAppendingFormat:@" %@ (%@, %@)", asciiArtDescription, [self.firstItem tag], [self.secondItem tag]]; 
  9.   
  10. #endif 
  11.   
  12. @end 
 
如果是整數的屬性標籤信息是不夠的,我們還可以得到更多新奇的東西,爲視圖類增加我們自己命名的屬性,然後可以打印到錯誤消息中。我們甚至可以在Interface Builder中,使用identity inspector中的 “User Defined Runtime Attributes”爲自定義屬性分配值。
  1. @interface UIView (AutoLayoutDebugging) 
  2. - (void)setAbc_NameTag:(NSString *)nameTag; 
  3. - (NSString *)abc_nameTag; 
  4. @end 
  5.   
  6. @implementation UIView (AutoLayoutDebugging) 
  7. - (void)setAbc_NameTag:(NSString *)nameTag 
  8.     objc_setAssociatedObject(self, "abc_nameTag", nameTag, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
  9.   
  10. - (NSString *)abc_nameTag 
  11.     return objc_getAssociatedObject(self, "abc_nameTag"); 
  12. @end 
  13.   
  14. @implementation NSLayoutConstraint (AutoLayoutDebugging) 
  15. #ifdef DEBUG 
  16. - (NSString *)description 
  17.     NSString *description = super.description; 
  18.     NSString *asciiArtDescription = self.asciiArtDescription; 
  19.     return [description stringByAppendingFormat:@" %@ (%@, %@)", asciiArtDescription, [self.firstItem abc_nameTag], [self.secondItem abc_nameTag]]; 
  20. #endif 
  21. @end 
 
通過這種方法錯誤消息變得更可讀,並且你不需要找出內存地址對應的視圖。然而,對你而言,你需要做一些額外的工作以確保每次爲視圖分配的名字都是有意義。
 
另一個技巧爲你提供更好的錯誤消息並且不需要額外的工作:對於每個佈局約束條件,都需要將調用棧的標誌融入到錯誤消息中。這樣就很容易看出來問題涉及到的約束了。要做到這一點,你需要swizzle UIView或者NSView的addConstraint:/addConstraints:方法,以及佈局約束的描述方法。在添加約束的方法中,你需要爲每個約束條件關聯一個對象,這個對象描述了當前調用棧堆棧的第一個frame。(或者任何你從中得到的信息):
  1. static void AddTracebackToConstraints(NSArray *constraints) 
  2.     NSArray *a = [NSThread callStackSymbols]; 
  3.     NSString *symbol = nil; 
  4.     if (2 < [a count]) 
  5.     { 
  6.         NSString *line = a[2]; 
  7.     // Format is 
  8.     //           1         2         3         4         5 
  9.     // 012345678901234567890123456789012345678901234567890123456789 
  10.     // 8 MyCoolApp 0x0000000100029809 -[MyViewController loadView] + 99 // 
  11.     // Don't add if this wasn't called from "MyCoolApp": 
  12.     if (59 <= [line length]) 
  13.     { 
  14.         line = [line substringFromIndex:4]; 
  15.         if ([line hasPrefix:@"My"]) { 
  16.         symbol = [line substringFromIndex:59 - 4]; 
  17.         } 
  18.     } 
  19.     for (NSLayoutConstraint *c in constraints) { 
  20.         if (symbol != nil) { 
  21.         objc_setAssociatedObject(c, &ObjcioLayoutConstraintDebuggingShort, symbol, OBJC_ASSOCIATION_COPY_NONATOMIC); 
  22.         } 
  23.         objc_setAssociatedObject(c, &ObjcioLayoutConstraintDebuggingCallStackSymbols, a, OBJC_ASSOCIATION_COPY_NONATOMIC); 
  24.     } 
  25. } @end 
 
一旦你已經爲每個約束對象提供這些信息,你可以簡單的修改UILayoutConstraint的描述方法將其包含到輸出日誌中。
  1. - (NSString *)objcioOverride_description { 
  2.     // call through to the original, really 
  3.     NSString *description = [self objcioOverride_description]; 
  4.     NSString *objcioTag = objc_getAssociatedObject(self, &ObjcioLayoutConstraintDebuggingShort); 
  5.     if (objcioTag == nil) { 
  6.         return description; 
  7.     } 
  8.     return [description stringByAppendingFormat:@" %@", objcioTag]; 
檢出這個GitHub倉庫,瞭解這一技術的代碼示例。
 
有歧義的佈局
另一個常見的問題就是有歧義的佈局。如果我們忘記添加一個約束條件,我們經常會想爲什麼佈局看起來不像我們所期望的那樣。UIView和NSView提供三種方式來查明有歧義的佈局:hasAmbiguousLayout,exerciseAmbiguityInLayout,和私有方法_autolayoutTrace。
 
顧名思義,如果視圖存在有歧義的佈局,那麼hasAmbiguousLayout返回YES。我們可以使用私有方法_autolayoutTrace,而不需要自己遍歷視圖層並記錄這個值。這將返回一個描述整個視圖樹的字符串→類似於recursiveDescription(當視圖存在有歧義的佈局時,這個方法會告訴你)。
 
由於這個方法是私有的,確保正式產品裏面不要包含這個方法調用的任何代碼。爲了防止你犯這種錯誤,你可以在視圖的category中這樣做:
  1. @implementation UIView (AutoLayoutDebugging) 
  2. - (void)printAutoLayoutTrace { 
  3.     #ifdef DEBUG 
  4.     NSLog(@"%@", [self performSelector:@selector(_autolayoutTrace)]); 
  5.     #endif 
  6. @end 
 
_autolayoutTrace打印的結果如下:
 
正如不可滿足約束條件的錯誤消息一樣,我們仍然需要弄明白打印出的內存地址所對應的視圖。
 
另一個標識出有歧義佈局更直觀的方法就是使用exerciseAmbiguityInLayout。這將會在有效值之間隨機改變視圖的frame。然而,每次調用這個方法只會改變frame一次。所以當你啓動程序的時候,你根本不會看到改變。創建一個遍歷所有視圖層級的輔助方法是一個不錯的主意,並且讓所有的視圖都有一個歧義的佈局“jiggle”。
  1. @implementation UIView (AutoLayoutDebugging) 
  2. - (void)exerciseAmiguityInLayoutRepeatedly:(BOOL)recursive { 
  3.     #ifdef DEBUG 
  4.     if (self.hasAmbiguousLayout) { 
  5.         [NSTimer scheduledTimerWithTimeInterval:.5 
  6.                                          target:self 
  7.                                        selector:@selector(exerciseAmbiguityInLayout) 
  8.                                        userInfo:nil 
  9.                                         repeats:YES]; 
  10.     } 
  11.     if (recursive) { 
  12.         for (UIView *subview in self.subviews) { 
  13.             [subview exerciseAmbiguityInLayoutRepeatedly:YES]; 
  14.         } 
  15.     } 
  16.     #endif 
  17. } @end 
NSUserDefault選項
有幾個有用的NSUserDefault選項可以幫助我們調試、測試自動佈局。你可以在代碼中設定,或者你也可以在scheme editor中指定它們作爲啓動參數。 顧名思義,UIViewShowAlignmentRects和NSViewShowAlignmentRects設置視圖可見的alignment rects。NSDoubleLocalizedStrings簡單的獲取並複製每個本地化的字符串。這是一個測試更長語言佈局的好方法。(谷了一張圖告訴你什麼是NSDoubleLocalizedStrings):
 
最後,設置AppleTextDirection和NSForceRightToLeftWritingDirection爲YES,來模擬從右到左的語言。
 
約束條件代碼
當在代碼中設置視圖和他們的約束條件時候,一定要記得將translatesAutoResizingMaskIntoConstraints設置爲NO。如果忘記設置這個屬性幾乎肯定會導致不可滿足的約束條件錯誤。即使你已經用自動佈局一段時間了,但還是要小心這個問題,因爲很容易在不經意間發生產生這個錯誤。
 
當你使用visual format language設置約束條件時,constraintsWithVisualFormat:options:metrics:views:方法有一個很有用的參數選擇。如果你還沒有用過,請參見文檔。這不同於格式化字符串只能影響一個視圖,它允許你調整在一定範圍內的視圖。舉個例子,如果用可視格式語言指定水平佈局,那麼你可以使用NSLayoutFormatAlignAllTop排列可視語言裏所有視圖爲上邊緣對齊。
 
還有一個使用可視格式語言在父視圖中居中子視圖的小技巧,這技巧利用了不均等約束和可選參數。下面的代碼在父視圖中水平排列了一個視圖:
  1. UIView *superview = theSuperView; 
  2. NSDictionary *views = NSDictionaryOfVariableBindings(superview, subview); 
  3. NSArray *c = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[superview]-(<=1)-[subview]"] options:NSLayoutFormatAlignAllCenterX metrics:nil views:views]; 
  4. [superview addConstraints:c]; 
 
這利用了NSLayoutFormatAlignAllCenterX選項在父視圖和子視圖間創建了居中約束。格式化字符串本身只是一個虛擬的東西,它會產生一個指定的約束,通常情況下只要子視圖是可見的,那麼父視圖底部和子視圖頂部邊緣之間的空間就應該小於等於1點。你可以顛倒示例中的方向達到垂直居中的效果。
 
使用可視格式語言另一個方便的輔助方法就是我們在上面例子中已經使用過的NSDictionaryFromVariableBindings宏指令,你傳遞一個可變數量的變量過去,返回得到一個鍵爲變量名的字典。
 
爲了佈局任務,你需要一遍一遍的調試,你可以方便的創建自己的輔助方法。比如,水平排列視圖時,你經常需要根據固定距離垂直的隔開一對相同類型的視圖,用下面的方法將會方便很多:
  1. @implementation UIView (AutoLayoutHelpers) 
  2. + leftAlignAndVerticallySpaceOutViews:(NSArray *)views distance:(CGFloat)distance 
  3.     for (NSUInteger i = 1; i < views.count; i++) { 
  4.         UIView *firstView = views[i - 1]; 
  5.         UIView *secondView = views[i]; 
  6.         firstView.translatesAutoResizingMaskIntoConstraints = NO; 
  7.         secondView.translatesAutoResizingMaskIntoConstraints = NO; 
  8.         NSLayoutConstraint *c1 = constraintWithItem:firstView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:secondView attribute:NSLayoutAttributeTop multiplier:1 constant:distance]; 
  9.         NSLayoutConstraint *c2 = constraintWithItem:firstView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:secondView attribute:NSLayoutAttributeLeading multiplier:1 constant:0]; 
  10.         [firstView.superview addConstraints:@[c1, c2]]; 
  11.     } 
  12.   
  13. @end 
 
同時也有許多不同的自動佈局幫助庫採用了不同的方法來簡化約束條件代碼。
 
性能
自動佈局是佈局過程中額外的一個步驟。它需要一組約束條件,並把這些約束條件轉換成frame。因此這自然會產生一些性能的影響。你需要知道的是,在絕大數情況下,如果你處理了非常關鍵的視圖代碼,那麼它用來解決約束條件系統的時間是可以忽略不計的。
 
例如,有一個collection view,當新出現一行時,你需要在屏幕上呈現幾個新的cell,並且每個cell包含幾個基於自動佈局的子視圖,這時你需要注意你的性能了。幸運的是,我們不需要用直覺來感受上下滾動的性能。啓動Instruments真實的測量一下自動佈局消耗的時間。當心NSISEngine類的方法。
 
另一種情況就是當你一次顯示大量視圖時可能會有性能問題。將約束條件轉換成視圖的frame時,解釋算法是超線性複雜的。這意味着當有一定數量的視圖時,性能將會變得非常低下。確切的數目取決於你具體使用情況和視圖配置。但是,給你一個粗略的概念,在當前iOS設備下,這個數字大概是100。你可以讀這兩個博客瞭解更多的細節(1,2)。
 
記住,這些都是極端的情況,不要過早的優化,並且避免自動佈局潛在的性能影響。這樣大多數情況便不會有問題。但是如果你懷疑這花費了你完全流暢地加載用戶界面的時間,分析你的代碼,然後你再去考慮用回手動設置frame有沒有意義。此外,硬件將會變得越來越能幹,並且Apple也會繼續調整自動佈局的性能。所以現實世界中極端情況的性能問題也將隨着時間減少。
 
結論
自動佈局是一個創建靈活用戶界面的強大功能,這種技術不會很快消失。剛開始使用自動佈局時可能會有點困難,但總會有柳暗花明的一天。一旦你掌握了這種技術,並且掌握了排錯的小技巧,便可庖丁解牛,恍然大悟:這™太符合邏輯了。

發佈了16 篇原創文章 · 獲贊 6 · 訪問量 17萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章