原文鏈接:http://www.objc.io/issue-3/advanced-auto-layout-toolbox.html
在我的上一個項目中,因為是面向公司內部使用的客戶端,所以我直接拋棄了iOS5,在項目中大量使用了iOS6中的新特性:自動布局,才發現生活可以如此美好(除了調bug的時候),發張圖大家感受一下,下面分別為橫屏和豎屏下的布局,再也不要像之前那樣適配的死去活來了:
這篇文章並沒有具體介紹自動布局的一些基本概念,主要講解了一些高級的使用方法和調試技巧,文中有的句子比較長,意思也有點難懂,所以需要靜下心仔細揣摩。如果你剛接觸自動布局,推薦你先看這幾篇文章:1.官方的Guide:http://t.cn/zR4naOF 2.來自raywenderlich的tutorial:http://t.cn/zlZTAZB 3.來自@onevcat 的blog:http://t.cn/zQnBspH。
##############################以下是正文###########################
自動布局在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
2
3
4
5
6
7
8
9
10
11
12
13
14
|
- layoutSubviews
{
[
super
layoutSubviews];
if
(
self
.subviews[0].frame.size.width <= MINIMUM_WIDTH)
{
[
self
removeSubviewConstraints];
self
.layoutRows += 1; [
super
layoutSubviews];
}
}
- updateConstraints
{
[
super
updateConstraints];
// add constraints depended on self.layoutRows...
}
|
多行文本的固有內容大小
UILabel和NSTextField對於多行文本的固有內容大小是模糊不清的。文本的高度取決於線的寬度,這也是解決約束條件時需要弄清的問題。為了解決這個問題,這兩個類都有一個叫做preferredMaxLayoutWidth的新屬性,這個屬性指定了線寬度的最大值,以便計算固有內容大小。
因為我們通常不能提前知道這個值,為了獲得正確的值我們需要先做兩步操作。首先,我們讓自動布局做它的工作,然后用布局傳遞結果的frame更新給首選最大寬度,並且再次觸發布局。
1
2
3
4
5
6
7
8
9
|
- (
void
)layoutSubviews
{
[
super
layoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[
super
layoutSubviews];
}
|
第一次調用[super layoutSubviews]是為了獲得label的frame,而第二次調用是改變后更新布局。如果省略第二個調用我們將會得到一個NSInternalInconsistencyException的錯誤,因為我們改變了更新約束條件的布局傳遞,但我們並沒有再次觸發布局。
我們也可以在label子類本身中這樣做:
1
2
3
4
5
6
7
8
|
@implementation
MyLabel
- (
void
)layoutSubviews
{
self
.preferredMaxLayoutWidth =
self
.frame.size.width;
[
super
layoutSubviews];
}
@end
|
在這種情況下,我們不需要先調用[super layoutSubviews],因為當layoutSubviews被調用時,label就已經有一個frame了。
為了在視圖控制器層級做出這樣的調整,我們用進入到viewDidLayoutSubviews。這時候第一個自動布局傳遞的frame已經被設置,我們可以用他們來設置首選最大寬度。
1
2
3
4
5
6
|
- (
void
)viewDidLayoutSubviews
{
[
super
viewDidLayoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[
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
2
3
|
[UIView animateWithDuration:1 animations:^{
myView.frame = newFrame;
}];
|
你現在需要寫:
1
2
3
4
|
// update constraints
[UIView animateWithDuration:1 animations:^{
[myView layoutIfNeeded];
}];
|
請注意,使用這種方法,你可以對約束條件做出的改變並不局限於約束條件的常量。你可以刪除約束條件,增加約束條件,甚至使用臨時動畫約束條件。由於新的約束只被解釋一次來決定新的frames,所以更復雜的布局改變都是有可能的。
需要記住的是:Core Animation和Auto Layout結合在一起產生視圖動畫時,自己不要接觸視圖的frame。一旦視圖使用自動布局,那么你已經將設置frame的責任交給了布局系統。你的干擾將造成怪異的行為。
這也意味着,如果自動布局改變視圖的frame,使用自動布局的視圖變換也不一定總是運行良好的。考慮下面這個例子:
1
2
3
|
[UIView animateWithDuration:1 animations:^{
myView.transform = CGAffineTransformMakeScale(.5, .5);
}];
|
通常我們期望這個方法在在保持視圖的中心時,將它的大小縮小到原來的一半。但是自動布局的行為是根據我們建立的約束條件種類來放置視圖。如果我們將其居中於它的父視圖,結果便像我們預想的一樣,因為應用視圖變換會觸發一個在父視圖內居中新frame的布局傳遞。然而,如果我們將視圖的左邊緣對齊到另一個視圖,那么這個alignment將會粘連住,並且中心點將會移動。
不管怎么樣,即使最初的結果跟我們預想的一樣,像這樣通過約束條件將轉換應用到視圖布局上並不是一個好主意。視圖的frame沒有和約束條件同步,也將導致怪異的行為。
如果你想使用轉換來產生視圖動畫或者直接使他的frame動態化,最干凈利索的技術是將這個視圖嵌入到一個視圖容器內,然后你可以在容器內重寫layoutSubviews,要么選擇完全脫離自動布局,要么僅僅調整他的結果。舉個例子,如果我們在我們的容器內建立一個子視圖,它根據容器的頂部和左邊緣自動布局,當布局根據以上的設置縮放轉換后我們可以調整它的中心:
1
2
3
4
5
6
7
8
9
10
11
12
|
- (
void
)layoutSubviews
{
[
super
layoutSubviews];
static
CGPoint center = {0,0};
if
(CGPointEqualToPoint(center, CGPointZero)) {
// grab the view's center point after initial layout
center =
self
.animatedView.center;
}
else
{
// apply the previous center to the animated view
self
.animatedView.center = center;
}
}
|
如果我們將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
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@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
|
如果是整數的屬性標簽信息是不夠的,我們還可以得到更多新奇的東西,為視圖類增加我們自己命名的屬性,然后可以打印到錯誤消息中。我們甚至可以在Interface Builder中,使用identity inspector中的 “User Defined Runtime Attributes”為自定義屬性分配值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@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
|
通過這種方法錯誤消息變得更可讀,並且你不需要找出內存地址對應的視圖。然而,對你而言,你需要做一些額外的工作以確保每次為視圖分配的名字都是有意義。
另一個技巧為你提供更好的錯誤消息並且不需要額外的工作:對於每個布局約束條件,都需要將調用棧的標志融入到錯誤消息中。這樣就很容易看出來問題涉及到的約束了。要做到這一點,你需要swizzle UIView或者NSView的addConstraint:/addConstraints:方法,以及布局約束的描述方法。在添加約束的方法中,你需要為每個約束條件關聯一個對象,這個對象描述了當前調用棧堆棧的第一個frame。(或者任何你從中得到的信息):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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的描述方法將其包含到輸出日志中。
1
2
3
4
5
6
7
8
9
|
- (
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提供三種方式來查明有歧義的布局:hasAmbiguousLayout,exerciseAmbiguityInLayout,和私有方法_autolayoutTrace。
顧名思義,如果視圖存在有歧義的布局,那么hasAmbiguousLayout返回YES。我們可以使用私有方法_autolayoutTrace,而不需要自己遍歷視圖層並記錄這個值。這將返回一個描述整個視圖樹的字符串→類似於recursiveDescription(當視圖存在有歧義的布局時,這個方法會告訴你)。
由於這個方法是私有的,確保正式產品里面不要包含這個方法調用的任何代碼。為了防止你犯這種錯誤,你可以在視圖的category中這樣做:
1
2
3
4
5
6
7
|
@implementation
UIView (AutoLayoutDebugging)
- (
void
)printAutoLayoutTrace {
#ifdef DEBUG
NSLog
(
@"%@"
, [
self
performSelector:
@selector
(_autolayoutTrace)]);
#endif
}
@end
|
_autolayoutTrace打印的結果如下:
正如不可滿足約束條件的錯誤消息一樣,我們仍然需要弄明白打印出的內存地址所對應的視圖。
另一個標識出有歧義布局更直觀的方法就是使用exerciseAmbiguityInLayout。這將會在有效值之間隨機改變視圖的frame。然而,每次調用這個方法只會改變frame一次。所以當你啟動程序的時候,你根本不會看到改變。創建一個遍歷所有視圖層級的輔助方法是一個不錯的主意,並且讓所有的視圖都有一個歧義的布局“jiggle”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@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簡單的獲取並復制每個本地化的字符串。這是一個測試更長語言布局的好方法。(谷了一張圖告訴你什么是NSDoubleLocalizedStrings):
最后,設置AppleTextDirection和NSForceRightToLeftWritingDirection為YES,來模擬從右到左的語言。
約束條件代碼
當在代碼中設置視圖和他們的約束條件時候,一定要記得將translatesAutoResizingMaskIntoConstraints設置為NO。如果忘記設置這個屬性幾乎肯定會導致不可滿足的約束條件錯誤。即使你已經用自動布局一段時間了,但還是要小心這個問題,因為很容易在不經意間發生產生這個錯誤。
當你使用visual format language設置約束條件時,constraintsWithVisualFormat:options:metrics:views:方法有一個很有用的參數選擇。如果你還沒有用過,請參見文檔。這不同於格式化字符串只能影響一個視圖,它允許你調整在一定范圍內的視圖。舉個例子,如果用可視格式語言指定水平布局,那么你可以使用NSLayoutFormatAlignAllTop排列可視語言里所有視圖為上邊緣對齊。
還有一個使用可視格式語言在父視圖中居中子視圖的小技巧,這技巧利用了不均等約束和可選參數。下面的代碼在父視圖中水平排列了一個視圖:
1
2
3
4
|
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宏指令,你傳遞一個可變數量的變量過去,返回得到一個鍵為變量名的字典。
為了布局任務,你需要一遍一遍的調試,你可以方便的創建自己的輔助方法。比如,水平排列視圖時,你經常需要根據固定距離垂直的隔開一對相同類型的視圖,用下面的方法將會方便很多:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@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。你可以讀這兩個博客了解更多的細節(1,2)。
記住,這些都是極端的情況,不要過早的優化,並且避免自動布局潛在的性能影響。這樣大多數情況便不會有問題。但是如果你懷疑這花費了你完全流暢地加載用戶界面的時間,分析你的代碼,然后你再去考慮用回手動設置frame有沒有意義。此外,硬件將會變得越來越能干,並且Apple也會繼續調整自動布局的性能。所以現實世界中極端情況的性能問題也將隨着時間減少。
結論
自動布局是一個創建靈活用戶界面的強大功能,這種技術不會很快消失。剛開始使用自動布局時可能會有點困難,但總會有柳暗花明的一天。一旦你掌握了這種技術,並且掌握了排錯的小技巧,便可庖丁解牛,恍然大悟:這™太符合邏輯了。