自動布局在 OS X 10.7 中被引進押桃,一年后在 iOS 6 中也可以用了。不久在 iOS 7 中的程序?qū)型O置全局字體大小导犹,因此除了不同的屏幕大小和方向唱凯,用戶界面布局也需要更大的靈活性。Apple 也在自動布局上花了很大功夫谎痢,所以如果你還沒做過這一塊磕昼,那么現(xiàn)在就是接觸這個技術(shù)的好時機。
很多開發(fā)者在第一次嘗試使用這個技術(shù)時都非常掙扎,因為用 Xcode 4 的 Interface Builder 建立 constraint-based layouts 體驗非常糟糕,但不要因為這個而灰心琼梆。自動布局其實比現(xiàn)在 Interface Builder 所支持的要好很多娜庇。Xcode 5 在這塊中將會帶來重要的變化栅隐。
這篇文章不是用來介紹 Auto Layout 的。如果你還沒用過它,那還是先去 WWDC 2012 看看基礎教程吧。(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é)一下自動布局將視圖顯示到屏幕上的步驟狭瞎。當你根據(jù)自動布局盡力寫出你想要的布局種類時,特別是高級的使用情況和動畫她君,這有利于后退一步脚作,并回憶布局過程是怎么工作的。
和 springs缔刹,struts 比起來球涛,在視圖被顯示之前,自動布局引入了兩個額外的步驟:更新約束 (updating constraints) 和布局視圖 (laying out views)校镐。每一步都是依賴前一步操作的亿扁;顯示依賴于布局視圖,布局視圖依賴于更新約束鸟廓。
第一步:更新約束从祝,可以被認為是一個“計量傳遞 (measurement pass)”。這是自下而上(從子視圖到父視圖)發(fā)生的引谜,它為布局準備好必要的信息牍陌,而這些布局將在實際設置視圖的 frame 時被傳遞過去并被使用。你可以通過調(diào)用 setNeedsUpdateConstraints 來觸發(fā)這個操作员咽,同時毒涧,你對約束條件系統(tǒng)做出的任何改變都將自動觸發(fā)這個方法。無論如何贝室,通知自動布局關于自定義視圖中任何可能影響布局的改變是非常有用的契讲。談到自定義視圖,你可以在這個階段重寫 updateConstraints 來為你的視圖增加需要的本地約束滑频。
第二步:布局捡偏,這是個自上而下(從父視圖到子視圖)的過程,這種布局操作實際上是通過設置 frame(在 OS X 中)或者 center 和 bounds(在 iOS 中)將約束條件系統(tǒng)的解決方案應用到視圖上峡迷。你可以通過調(diào)用 setNeedsLayout 來觸發(fā)一個操作請求银伟,這并不會立刻應用布局,而是在稍后再進行處理绘搞。因為所有的布局請求將會被合并到一個布局操作中去枣申,所以你不需要為經(jīng)常調(diào)用這個方法而擔心。
你可以調(diào)用layoutIfNeeded/layoutSubtreeIfNeeded(分別針對 iOS / OS X)來強制系統(tǒng)立即更新視圖樹的布局看杭。如果你下一步操作依賴于更新后視圖的 frame忠藤,這將非常有用。在你自定義的視圖中楼雹,你可以重寫layoutSubviews/layout來獲得控制布局變化的所有權(quán)模孩,我們稍后將展示使用方法。
最終贮缅,不管你是否用了自動布局榨咐,顯示器都會自上而下將渲染后的視圖傳遞到屏幕上,你也可以通過調(diào)用setNeedsDisplay來觸發(fā)谴供,這將會導致所有的調(diào)用都被合并到一起推遲重繪块茁。重寫熟悉的drawRect:能夠讓我們獲得自定義視圖中顯示過程的所有權(quán)。
既然每一步都是依賴前一步操作的,如果有任何布局的變化還沒實行的話数焊,顯示操作將會觸發(fā)一個布局行為永淌。類似地,如果約束條件系統(tǒng)中存在沒有實行的改變佩耳,布局變化也將會觸發(fā)更新約束條件遂蛀。
需要牢記的是,這三步并不是單向的干厚±畹危基于約束條件的布局是一個迭代的過程,布局操作可以基于之前的布局方案來對約束做出更改蛮瞄,而這將再次觸發(fā)約束的更新所坯,并緊接另一個布局操作。這可以被用來創(chuàng)建高級的自定義視圖布局挂捅,但是如果你每一次調(diào)用的自定義layoutSubviews都會導致另一個布局操作的話芹助,你將會陷入到無限循環(huán)的麻煩中去。
為自定義視圖啟動自動布局
當創(chuàng)建一個自定義視圖時籍凝,你需要知道關于自動布局的這些事情:具體指定一個恰當?shù)墓逃袃?nèi)容尺寸 (intrinsic content size)周瞎,區(qū)分開視圖的 frame 和 alignment rect,啟動 baseline-aligned 布局饵蒂,如何 hook 到布局過程中声诸,我們將會逐一了解這些部分。
固有內(nèi)容尺寸(Intrinsic Content Size )
固有內(nèi)容尺寸是一個視圖期望為其顯示特定內(nèi)容得到的大小退盯。比如彼乌,UILabel有一個基于字體的首選高度,一個基于字體和顯示文本的首選寬度渊迁。UIProgressView僅有一個基于其插圖的首選高度慰照,但沒有首選寬度。一個沒有格式的UIView既沒有首選寬度也沒有首選高度琉朽。
你需要根據(jù)想要顯示的內(nèi)容來決定你的自定義視圖是否具有一個固有內(nèi)容尺寸毒租,如果有的話,它是在哪個尺度上固有箱叁。
為了在自定義視圖中實現(xiàn)固有內(nèi)容尺寸墅垮,你需要做兩件事:重寫intrinsicContentSize為內(nèi)容返回恰當?shù)拇笮。瑹o論何時有任何會影響固有內(nèi)容尺寸的改變發(fā)生時耕漱,調(diào)用invalidateIntrinsicContentSize算色。如果這個視圖只有一個方向的尺寸設置了固有尺寸,那么為另一個方向的尺寸返回UIViewNoIntrinsicMetric/NSViewNoIntrinsicMetric螟够。
需要注意的是灾梦,固有內(nèi)容尺寸必須是獨立于視圖 frame 的峡钓。例如,不可能返回一個基于 frame 高度或?qū)挾鹊奶囟ǜ邔挶鹊墓逃袃?nèi)容尺寸若河。
壓縮阻力 (Compression Resistance) 和 內(nèi)容吸附 (Content Hugging)
!!!!譯者注:我理解為壓縮阻力和內(nèi)容吸附性能岩,實在是想不到更貼切的名稱了。壓縮阻力是控制視圖在兩個方向上的收縮性牡肉,內(nèi)容吸附性是當視圖的大小改變時捧灰,它會盡量讓視圖靠近它的固有內(nèi)容尺寸
每個視圖在兩個方向上都分配有內(nèi)容壓縮阻力優(yōu)先級和內(nèi)容吸附性優(yōu)先級淆九。只有當視圖定義了固有內(nèi)容尺寸時這些屬性才能起作用统锤,如果沒有定義內(nèi)容大小,那就沒法阻止被壓縮或者吸附了炭庙。
在后臺中饲窿,固有內(nèi)容尺寸和這些優(yōu)先值被轉(zhuǎn)換為約束條件。一個固有內(nèi)容尺寸為{100焕蹄,30}的 label逾雄,水平/垂直壓縮阻力優(yōu)先值為750,水平/垂直的內(nèi)容吸附性優(yōu)先值為250腻脏,這四個約束條件將會生成:
如果你不熟悉上面約束條件所使用的可視格式語言鸦泳,你可以到Apple 文檔中了解。記住永品,這些額外的約束條件對了解自動布局的行為產(chǎn)生了隱式的幫助做鹰,同時也更好理解它的錯誤信息。
Frame 和 Alignment Rect
自動布局并不會操作視圖的 frame鼎姐,但能作用于視圖的 alignment rect钾麸。大家很容易忘記它們之間細微的差別,因為在很多情況下炕桨,它們是相同的饭尝。但是alignment rect 實際上是一個強大的新概念:從一個視圖的視覺外觀解耦出視圖的 layout alignment edges。
比如献宫,一個自定義 icon 類型的按鈕比我們期望點擊目標還要小的時候钥平,這將會很難布局。當插圖顯示在一個更大的 frame 中時姊途,我們將不得不了解它顯示的大小涉瘾,并且相應調(diào)整按鈕的 frame,這樣 icon 才會和其他界面元素排列好吭净。當我們想要在內(nèi)容的周圍繪制像 badges睡汹,陰影,倒影的裝飾時寂殉,也會發(fā)生同樣的情況囚巴。
我們可以使用 alignment rect 簡單的定義需要用來布局的矩形。在大多數(shù)情況下,你僅需要重寫alignmentRectInsets方法彤叉,這個方法允許你返回相對于 frame 的 edge insets庶柿。如果你需要更多控制權(quán),你可以重寫alignmentRectForFrame:和frameForAlignmentRect:秽浇。如果你不想減去固定的 insets浮庐,而是計算基于當前 frame 的 alignment rect,那么這兩個方法將會非常有用柬焕。但是你需要確保這兩個方法是互為可逆的审残。
關于這點,回憶上面提及到的視圖固有內(nèi)容尺寸引用它的 alignment rect斑举,而不是 frame搅轿。這是有道理的,因為自動布局直接根據(jù)固有內(nèi)容尺寸產(chǎn)生壓縮阻力和內(nèi)容吸附約束條件富玷。
基線對齊 (Baseline Alignment)
為了讓使用NSLayoutAttributeBaseline屬性的約束條件對自定義視圖奏效璧坟,我們需要做一些額外的工作。當然赎懦,這只有我們討論的自定義視圖中有類似基準線的東西時雀鹃,才有意義。
在 iOS 中励两,可以通過實現(xiàn)viewForBaselineLayout來激活基線對齊黎茎。在這里返回的視圖底邊緣將會作為 基線。默認實現(xiàn)只是簡單的返回自己伐蒋,然而自定義的實現(xiàn)可以返回任何子視圖工三。在 OS X 中,你不需要返回一個子視圖先鱼,而是重新定義baselineOffsetFromBottom返回一個從視圖底部邊緣開始的 offset俭正,這和在 iOS 中一樣,默認實現(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ā)更新約束條件的操作衙四。
控制子視圖布局
如果你不能利用布局約束條件達到子視圖預期的布局铃肯,你可以進一步在 iOS 里重寫layoutSubviews或者在 OS X 里面重寫layout。通過這種方式传蹈,當約束條件系統(tǒng)得到解決并且結(jié)果將要被應用到視圖中時押逼,你便已經(jīng)進入到布局過程的第二步。
最極端的情況是不調(diào)用父類的實現(xiàn)惦界,自己重寫全部的layoutSubviews / layout挑格。這就意味著你在這個視圖里的視圖樹里拋棄了自動布局。從現(xiàn)在起沾歪,你可以按喜歡的方式手動放置子視圖漂彤。
如果你仍然想使用約束條件布局子視圖,你需要調(diào)用[super layoutSubviews]/[super layout]灾搏,然后對布局進行微調(diào)挫望。你可以通過這種方式創(chuàng)建那些通過定于約束無法實現(xiàn)的布,比如狂窑,由到視圖大小之間的關系或是視圖之間間距的關系來定義的布局媳板。
這方面另一個有趣的使用案例就是創(chuàng)建一個布局依賴的視圖樹。當自動布局完成第一次傳遞并且為自定義視圖的子視圖設置好 frame 后泉哈,你便可以檢查子視圖的位置和大小蛉幸,并為視圖層級和(或)約束條件做出調(diào)整到旦。WWDC session 228 – Best Practices for Mastering Auto Layout有一個很好的例子。
你也可以在第一次布局操作完成后再決定改變約束條件巨缘。比如添忘,如果視圖變得太窄的話,將原來排成一行的子視圖轉(zhuǎn)變成兩行若锁。
多行文本的固有內(nèi)容尺寸
UILabel和NSTextField對于多行文本的固有內(nèi)容尺寸是模糊不清的搁骑。文本的高度取決于行的寬度,這也是解決約束條件時需要弄清的問題又固。為了解決這個問題仲器,這兩個類都有一個叫做preferredMaxLayoutWidth的新屬性,這個屬性指定了行寬度的最大值仰冠,以便計算固有內(nèi)容尺寸乏冀。
因為我們通常不能提前知道這個值,為了獲得正確的值我們需要先做兩步操作洋只。首先辆沦,我們讓自動布局做它的工作,然后用布局操作結(jié)果的 frame 更新給首選最大寬度识虚,并且再次觸發(fā)布局肢扯。
第一次調(diào)用[super layoutSubviews]是為了獲得 label 的 frame,而第二次調(diào)用是為了改變后更新布局担锤。如果省略第二個調(diào)用我們將會得到一個NSInternalInconsistencyException的錯誤蔚晨,因為我們改變了更新約束條件的布局操作,但我們并沒有再次觸發(fā)布局肛循。
我們也可以在 label 子類本身中這樣做:
在這種情況下铭腕,我們不需要先調(diào)用[super layoutSubviews],因為當layoutSubviews被調(diào)用時多糠,label 就已經(jīng)有一個 frame 了累舷。
為了在視圖控制器層級做出這樣的調(diào)整,我們用掛鉤到 viewDidLayoutSubviews熬丧。這時候第一個自動布局操作的 frame 已經(jīng)被設置笋粟,我們可以用它們來設置首選最大寬度。
最后析蝴,確保你沒有給 label 設置一個比 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 方法慢得多,這也使得它暫時不適合移動平臺逐哈。
當使用 Core Animation 方法時芬迄,即使不使用自動布局,動畫的工作方式在概念上也是一樣的昂秃。不同的是禀梳,你不需要手動設置視圖的目標 frames,取而代之的是修改約束條件并觸發(fā)一個布局操作為你設置 frames械蹋。在 iOS 中出皇,代替:
你現(xiàn)在需要寫:
請注意,使用這種方法哗戈,你可以對約束條件做出的改變并不局限于約束條件的常量。你可以刪除約束條件荷科,增加約束條件唯咬,甚至使用臨時動畫約束條件。由于新的約束只被解釋一次來決定新的 frames畏浆,所以更復雜的布局改變都是有可能的胆胰。
需要記住的是:Core Animation 和 Auto Layout 結(jié)合在一起產(chǎn)生視圖動畫時,自己不要接觸視圖的 frame刻获。一旦視圖使用自動布局蜀涨,那么你已經(jīng)將設置 frame 的責任交給了布局系統(tǒng)。你的干擾將造成怪異的行為蝎毡。
這也意味著厚柳,如果使用的視圖變換 (transform) 改變了視圖的 frame 的話,它和自動布局是無法一起正常使用的沐兵”鹂澹考慮下面這個例子:
通常我們期望這個方法在保持視圖的中心時,將它的大小縮小到原來的一半扎谎。但是自動布局的行為是根據(jù)我們建立的約束條件種類來放置視圖的碳想。如果我們將其居中于它的父視圖烧董,結(jié)果便像我們預想的一樣,因為應用視圖變換會觸發(fā)一個在父視圖內(nèi)居中新 frame 的布局操作胧奔。然而逊移,如果我們將視圖的左邊緣對齊到另一個視圖,那么這個 alignment 將會粘連住龙填,并且中心點將會移動螟左。
不管怎么樣,即使最初的結(jié)果跟我們預想的一樣觅够,像這樣通過約束條件將轉(zhuǎn)換應用到視圖布局上并不是一個好主意胶背。視圖的 frame 沒有和約束條件同步,也將導致怪異的行為喘先。
如果你想使用 transform 來產(chǎn)生視圖動畫或者直接使它的 frame 動態(tài)化钳吟,最干凈利索的技術(shù)是將這個視圖嵌入到一個視圖容器內(nèi),然后你可以在容器內(nèi)重寫 layoutSubviews窘拯,要么選擇完全脫離自動布局红且,要么僅僅調(diào)整它的結(jié)果。舉個例子涤姊,如果我們在我們的容器內(nèi)建立一個子視圖暇番,它根據(jù)容器的頂部和左邊緣自動布局,當布局根據(jù)以上的設置縮放轉(zhuǎn)換后我們可以調(diào)整它的中心:
如果我們將 animatedView 屬性暴露為 IBOutlet思喊,我們甚至可以使用 Interface Builder 里面的容器壁酬,并且使用約束條件放置它的的子視圖,同時還能夠根據(jù)固定的中心應用縮放轉(zhuǎn)換恨课。
調(diào)試
當談到調(diào)試自動布局舆乔,OS X 比 iOS 還有一個重要的優(yōu)勢。在 OS X 中剂公,你可以利用 Instrument 的 Cocoa Layout 模板希俩,或者是NSWindow的visualizeConstraints:方法。而且NSView有一個identifier屬性纲辽,為了獲得更多可讀的自動布局錯誤信息颜武,你可以在 Interface Builder 或代碼里面設置這個屬性。
不可滿足的約束條件
如果我們在 iOS 中遇到不可滿足的約束條件拖吼,我們只能在輸出的日志中看到視圖的內(nèi)存地址鳞上。尤其是在更復雜的布局中,有時很難辨別出視圖的哪一部分出了問題绿贞。然而因块,在這種情況下,還有幾種方法可以幫到我們籍铁。
首先涡上,當你在不可滿足的約束條件錯誤信息中看到NSLayoutResizingMaskConstraints時趾断,你肯定忘了為你某一個視圖設定translatesAutoResizingMaskIntoConstraints為 NO。Interface Builder 中會自動設置吩愧,但是使用代碼時芋酌,你需要為所有的視圖手動設置。
如果不是很明確是哪個視圖導致的問題雁佳,你就需要通過內(nèi)存地址來辨認視圖脐帝。最簡單的方法是使用調(diào)試控制臺。你可以打印視圖本身或它父視圖的描述糖权,甚至遞歸描述的樹視圖堵腹。這通常會提示你需要處理哪個視圖。
一個更直觀的方法是在控制臺修改有問題的視圖星澳,這樣你可以在屏幕上標注出來疚顷。比如,你可以改變它的背景顏色:
確保重新執(zhí)行你的程序禁偎,否則改變不會在屏幕上顯示出來腿堤。還要注意將內(nèi)存地址轉(zhuǎn)換為(UIView *),以及額外的圓括號如暖,這樣我們就可以使用點操作笆檀。另外,你當然也可以通過發(fā)送消息來實現(xiàn):
確保重新執(zhí)行你的程序盒至,否則改變不會在屏幕上顯示出來酗洒。還要注意將內(nèi)存地址轉(zhuǎn)換為(UIView *),以及額外的圓括號妄迁,這樣我們就可以使用點操作寝蹈。另外,你當然也可以通過發(fā)送消息來實現(xiàn):
另一種方法是使用 Instrument 的 allocation 模板登淘,根據(jù)圖表分析。一旦你從錯誤消息中得到內(nèi)存地址(運行 Instruments 時封字,你從 Console 應用中獲得的錯誤消息)黔州,你可以將 Instrument 的詳細視圖切換到 Objects List 頁面,并且用 Cmd-F 搜索那個內(nèi)存地址阔籽。這將會為你顯示分配視圖對象的方法流妻,這通常是一個很好的暗示(至少對那些由代碼創(chuàng)建的視圖來說是這樣的)。
你也可以通過改進錯誤信息本身笆制,來更容易地在 iOS 中弄懂不可滿足的約束條件錯誤到底在哪里绅这。我們可以在一個 category 中重寫NSLayoutConstraint的描述,并且將視圖的 tags 包含進去:
如果整數(shù)的tag屬性信息不夠的話在辆,我們還可以得到更多新奇的東西证薇,并且在視圖類中增加我們自己命名的屬性度苔,然后可以打印到錯誤消息中。我們甚至可以在 Interface Builder 中寞蚌,使用 identity 檢查器中的 “User Defined Runtime Attributes” 為自定義屬性分配值诡延。
通過這種方法錯誤消息變得更可讀惋增,并且你不需要找出內(nèi)存地址對應的視圖。然而甩骏,對你而言,你需要做一些額外的工作以確保每次為視圖分配的名字都是有意義先慷。
Daniel提出了另一個很巧妙的方法饮笛,可以為你提供更好的錯誤消息并且不需要額外的工作:對于每個布局約束條件,都需要將調(diào)用棧的標志融入到錯誤消息中论熙。這樣就很容易看出來問題涉及到的約束了福青。要做到這一點,你需要 swizzle UIView 或者 NSView 的addConstraint:/addConstraints:方法赴肚,以及布局約束的description方法素跺。在添加約束的方法中,你需要為每個約束條件關聯(lián)一個對象誉券,這個對象描述了當前調(diào)用棧堆棧的第一個棧頂信息(或者任何你從中得到的信息)
一旦你為每個約束對象提供這些信息指厌,你可以簡單的修改UILayoutConstraint的描述方法將其包含到輸出日志中。
檢出這個GitHub倉庫踊跟,了解這一技術(shù)的代碼示例踩验。
另一個常見的問題就是有歧義的布局。如果我們忘記添加一個約束條件商玫,我們經(jīng)常會想為什么布局看起來不像我們所期望的那樣箕憾。UIView和NSView提供三種方式來查明有歧義的布局:
,
拳昌,和私有方法_autolayoutTrace袭异。
顧名思義,如果視圖存在有歧義的布局炬藤,那么hasAmbiguousLayout返回YES御铃。如果我們不想自己遍歷視圖層并記錄這個值,可以使用私有方法 _autolayoutTrace沈矿。這將返回一個描述整個視圖樹的字符串:類似于recursiveDescription的輸出(當視圖存在有歧義的布局時上真,這個方法會告訴你)。
由于這個方法是私有的羹膳,確保正式產(chǎn)品里面不要包含調(diào)用這個方法的任何代碼睡互。為了防止你犯這種錯誤,你可以在視圖的category中這樣做:
_autolayoutTrace打印的結(jié)果如下:
正如不可滿足約束條件的錯誤消息一樣,我們?nèi)匀恍枰靼状蛴〕龅膬?nèi)存地址所對應的視圖就珠。
另一個標識出有歧義布局更直觀的方法就是使用exerciseAmbiguityInLayout寇壳。這將會在有效值之間隨機改變視圖的 frame。然而嗓违,每次調(diào)用這個方法只會改變 frame 一次九巡。所以當你啟動程序的時候,你根本不會看到改變蹂季。創(chuàng)建一個遍歷所有視圖層級的輔助方法是一個不錯的主意冕广,并且讓所有的視圖都有一個歧義的布局“晃動 (jiggle)”。
NSUserDefault選項
有幾個有用的NSUserDefault選項可以幫助我們調(diào)試偿洁、測試自動布局撒汉。你可以在代碼中設定,或者你也可以在scheme editor中指定它們作為啟動參數(shù)涕滋。
顧名思義睬辐,UIViewShowAlignmentRects和NSViewShowAlignmentRects設置視圖可見的 alignment rects。NSDoubleLocalizedStrings簡單的獲取并復制每個本地化的字符串宾肺。這是一個測試更長語言布局的好方法溯饵。最后,設置AppleTextDirection和NSForceRightToLeftWritingDirection為YES锨用,來模擬從右到左的語言丰刊。
編者注如果你不知道怎么在 scheme 中設置類似NSDoubleLocalizedStrings,這里有一張圖來說明增拥;
約束條件代碼
當在代碼中設置視圖和它們的約束條件時候啄巧,一定要記得將translatesAutoResizingMaskIntoConstraints設置為 NO。如果忘記設置這個屬性幾乎肯定會導致不可滿足的約束條件錯誤掌栅。即使你已經(jīng)用自動布局一段時間了秩仆,但還是要小心這個問題,因為很容易在不經(jīng)意間發(fā)生產(chǎn)生這個錯誤猾封。
當你使用可視化結(jié)構(gòu)語言 (visual format language, VFL)設置約束條件時澄耍,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:nilviews:views];[superview addConstraints:c];
這利用了NSLayoutFormatAlignAllCenterX選項在父視圖和子視圖間創(chuàng)建了居中約束嫩舟。格式化字符串本身只是一個虛擬的東西氢烘,它會產(chǎn)生一個指定的約束,通常情況下只要子視圖是可見的家厌,那么父視圖底部和子視圖頂部邊緣之間的空間就應該小于等于1點播玖。你可以顛倒示例中的方向達到垂直居中的效果。
使用可視格式語言另一個方便的輔助方法就是我們在上面例子中已經(jīng)使用過的 NSDictionaryFromVariableBindings 宏指令饭于,你傳遞一個可變數(shù)量的變量過去蜀踏,返回得到一個鍵為變量名的字典。
為了布局任務掰吕,你需要一遍一遍的調(diào)試果覆,你可以方便的創(chuàng)建自己的輔助方法。比如殖熟,你想要垂直地排列一系列視圖局待,想要它們垂直方向間距一致,水平方向上所有視圖以它們的左邊緣對齊菱属,用下面的方法將會方便很多:
同時也有許多不同的自動布局的庫采用了不同的方法來簡化約束條件代碼钳榨。
性能
自動布局是布局過程中額外的一個步驟。它需要一組約束條件纽门,并把這些約束條件轉(zhuǎn)換成 frame薛耻。因此這自然會產(chǎn)生一些性能的影響。你需要知道的是膜毁,在絕大數(shù)情況下昭卓,用來解決約束條件系統(tǒng)的時間是可以忽略不計的。但是如果你正好在處理一些性能關鍵的視圖代碼時瘟滨,最好還是對這一點有所了解候醒。
例如,有一個 collection view杂瘸,當新出現(xiàn)一行時倒淫,你需要在屏幕上呈現(xiàn)幾個新的 cell,并且每個 cell 包含幾個基于自動布局的子視圖败玉,這時你需要注意你的性能了敌土。幸運的是,我們不需要用直覺來感受上下滾動的性能运翼。啟動 Instruments 真實的測量一下自動布局消耗的時間返干。當心NSISEngine類的方法。
另一種情況就是當你一次顯示大量視圖時可能會有性能問題血淌。將約束條件轉(zhuǎn)換成視圖的 frame 時矩欠,用來計算約束的算法是超線性復雜的财剖。這意味著當有一定數(shù)量的視圖時,性能將會變得非常低下癌淮。而這確切的數(shù)目取決于你具體使用情況和視圖配置躺坟。但是,給你一個粗略的概念乳蓄,在當前 iOS 設備下咪橙,這個數(shù)字大概是 100。你可以讀這兩個博客帖子了解更多的細節(jié)虚倒。
記住美侦,這些都是極端的情況,不要過早的優(yōu)化裹刮,并且避免自動布局潛在的性能影響音榜。這樣大多數(shù)情況便不會有問題。但是如果你懷疑這花費了你決定性的幾十毫秒捧弃,從而導致用戶界面不完全流暢的話赠叼,分析你的代碼,然后你再去考慮用回手動設置 frame 有沒有意義违霞。此外嘴办,硬件將會變得越來越能干,并且Apple也會繼續(xù)調(diào)整自動布局的性能买鸽。所以現(xiàn)實世界中極端情況的性能問題也將隨著時間減少涧郊。
結(jié)論
自動布局是一個創(chuàng)建靈活用戶界面的強大功能,這種技術(shù)不會消失眼五。剛開始使用自動布局時可能會有點困難妆艘,但總會有柳暗花明的一天。一旦你掌握了這種技術(shù)看幼,并且掌握了排錯的小技巧批旺,便可庖丁解牛,恍然大悟:這太符合邏輯了诵姜。