翻譯自“Auto Layout Guide”。
4 高級自動布局
4.1 通過代碼創建約束
盡可能使用界面生成器設置約束。界面生成器提供了很多工具,可以可視化,編輯,管理和調試約束。通過分析約束,它還會在設計時發現很多常見的錯誤,讓你在運行應用程序之前修復它們。
界面生成器可以管理日益增長的任務。可以在界面生成器中直接構建幾乎任何類型的約束(查看“在界面生成器中使用約束”)。還可以指定具體尺寸類的約束(查看“調試自動布局”),以及使用新的工具,例如堆棧視圖,甚至還可以在運行時動態添加或移除視圖(查看”動態的堆棧視圖“)。然而,視圖層級結構的某些動態變化只能在代碼中管理。
通過代碼創建約束時,你有三個選擇:使用布局錨點,使用NSLayoutConstraint類,或者使用Visual Format Language。
4.1.1 布局錨點(Anchors)
NSLayoutAnchor類為創建約束提供了連貫(fluent)的接口。通過訪問要約束項的anchor屬性使用該API。例如,視圖控制到的頂部和底部布局向導有topAnchor,bottomAnchor和heightAnchor屬性。另一方面,視圖為邊緣(edge),中心(center),尺寸(size)和基線(baseline)暴露了錨點。
提示
在iOS中,視圖還有layoutMarginsGuide和readableContentGuide屬性。這些屬性暴露了UILayoutGuide對象,分別表示視圖的頁邊留白和可讀內容向導(readable content guides)。反過來,這些向導為邊緣,中心和尺寸暴露了錨點。
通過代碼創建約束到頁邊留白或可讀內容向導時,使用這些向導。
布局錨點讓你創建易讀的,緊湊格式的約束。它們暴露一系列方法創建不同類型的約束,如列表13-1所示。
列表13-1 創建布局錨點
// Get the superview's layout
let margins = view.layoutMarginsGuide
// Pin the leading edge of myView to the margin's leading edge
myView.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor).active = true
// Pin the trailing edge of myView to the margin's trailing edge
myView.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor).active = true
// Give myView a 1:2 aspect ratio
myView.heightAnchor.constraintEqualToAnchor(myView.widthAnchor, multiplier: 2.0)
正如“Anatomy of a Constraint”中描述的,一個約束是一個簡單的線性方程式。

布局錨點有不同的方法創建約束。每個方法只包括影響結果的方程式元素。所以,下面的代碼中:
myView.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor).active = true
符號相等于方程式的以下部分:
Equation | Symbol |
---|---|
Item 1 | myView |
Attribute 1 | leadingAnchor |
Relationship | constraintEqualToAnchor |
Multiplier | None (defaults to 1.0) |
Item 2 | margins |
Attribute 2 | leadingAnchor |
Constant | None (defaults to 0.0) |
布局錨點還提供了額外的類型安全。NSLayoutAnchor類有很多子類,為創建約束添加了類型信息,以及具體子類的方法。它幫助阻止意外創建無效的約束。例如,可以只約束水平錨點(leadingAnchor或trailingAnchor)到其它水平錨點。類似的,可以只為尺寸約束提供乘數。
提示
這些規則不是NSLayoutConstraint API的強制要求。相反,如果創建了一個無效的約束,該約束會在運行時拋出異常。因此,布局錨點幫助把運行時錯誤轉換為編譯時的錯誤。
更多信息請參考“NSLayoutAnchor Class Reference”。
4.1.2 NSLayoutConstraint類
你還可以直接使用NSLayoutConstraint類的constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:便利(convenience)方法創建約束。該方法顯式的把約束方程式轉換為代碼。每個參數對應方程式的一部分(參考“約束方程式”)。
不像布局錨點API提供的方法,該方法必須為每個參數指定值,即使它不影響布局。最終結果是相當多的引用代碼(boilerplate code),該代碼通常很難閱讀。例如,列表13-2中的代碼在功能上與列表13-1相同。
列表13-2 直接實例化約束
NSLayoutConstraint(item: myView, attribute: .Leading, relatedBy: .Equal, toItem: view, attribute: .LeadingMargin, multiplier: 1.0, constant: 0.0).active = true
NSLayoutConstraint(item: myView, attribute: .Trailing, relatedBy: .Equal, toItem: view, attribute: .TrailingMargin, multiplier: 1.0, constant: 0.0).active = true
NSLayoutConstraint(item: myView, attribute: .Height, relatedBy: .Equal, toItem: myView, attribute:.Width, multiplier: 2.0, constant:0.0).active = true
提示
在iOS中,NSLayoutAttribute枚舉包括視圖頁邊留白的值。這意味著你可以不通過layoutMarginsGuide屬性,就能創建到頁邊留白的約束。但是,你仍然需要使用readableContentGuide屬性,創建到可讀內容向導的約束。
不像布局錨點API,便利方法不會高亮具體約束的重要特征。因此,閱讀代碼時,更容易錯過重要的細節。另外,編譯器不會執行約束的任何靜態分析。你可以很自由的創建無效約束。然后這些約束在運行時拋出異常。因此,除非你需要支持iOS 8或者OS X v10.10,或者更早的版本,考慮遷移到更新的布局錨點API。
更多信息請參考“NSLayoutConstraint Class Reference”。
4.1.3 Visual Format Language
Visual Format Language讓你可以使用ASCII藝術(ASCII-art),比如字符串,來定義約束。這提供了約束的視覺描述。Visual Formatting Language有以下優點和缺點:
- 自動布局使用Visual Format Language在控制臺打印約束;基于這個原因,調試信息看起來很像創建約束的代碼。
- Visual Format Language讓你可以一次創建多個約束,通過使用一個簡潔的表達式。
- Visual Format Language只能創建有效的約束。
- 符號強調良好可視化的完整性。因此,有些約束(例如長寬度)不能使用Visual Format Language創建。
- 編譯器不會以任何方式驗證字符串。只能通過運行時測試發現錯誤。
使用Visual Format Language重寫列表13-1中的例子:
let views = ["myView" : myView]
let formatString = "|-[myView]-|"
let constraints = NSLayoutConstraint.constraintsWithVisualFormat(formatString, options:.AlignAllTop , metrics: nil, views: views)
NSLayoutConstraint.activateConstraints(constraints)
該例子同時創建和啟用開頭和結尾約束。使用默認間隔時,Visual Format Language總是創建到父視圖的頁邊留白10個點的約束,因此這些約束跟之前的例子相同。但是,列表13-3不能創建長寬比約束。
如果創建一行有多個項的復雜視圖,Visual Format Language同時制定垂直對齊和水平間隔。從字面上看,“Align All Top”選項對布局沒有影響,因為例子中只有一個視圖(不包括父視圖)。
使用Visual Format Language創建約束需要以下步驟:
- 創建views字典。該字典必須使用字符串作為key,視圖對象(或者自動布局中可以被約束的其它項,例如布局向導)作為值。使用格式字符串識別視圖。
提示
使用Objective-C時,使用NSDictionaryOfVariableBindings宏創建視圖字典。在Swift中,必須自己創建字典。
- (可選)創建度量(metrics)字典。該字典必須使用字符串作為key,NSNumber對象作為值。使用kye表示格式化字符串的常量值。
- 通過布局項的單行或單列創建格式化字符串。
- 調用NSLayoutConstraint類的constraintsWithVisualFormat:options:metrics:views:方法。該方法返回包括所有約束的數組。
- 調用NSLayoutConstraint類的activateConstraints:方法啟用約束。
更多信息請參考“Visual Format Language”附錄。
4.2 具體尺寸類的布局
界面生成器的故事版默認使用尺寸類。尺寸類是分配給用戶界面元素(比如場景或視圖)的特征(traits)。它們大致表示元素的尺寸。界面生成器根據當前尺寸類,讓你自定義很多布局特征。尺寸類變化時,布局自動適配。特別是,你可以在每個尺寸類基礎上設置以下特征:
- 安裝或卸載一個視圖或控件。
- 安裝或卸載一個約束。
- 設置選中屬性的值(例如,字體和布局頁邊留白設置)。
當系統加載場景時,它實例化所有視圖,控件和約束,并在視圖控制器中指定這些項的合適outlet(如果有的話)。不管場景的當前尺寸類,你都可以通過項的outlet訪問它們。但是,只有在項被安裝在當前尺寸類時,系統才會添加它們到視圖層級結構。
當視圖的尺寸類改變時(例如,旋轉iPhone,或者在全屏和分割視圖中切換iPad應用程序),系統自動添加項到視圖層級結構,或者從視圖層級結構中移除它們。系統也會動畫的改變視圖的布局。
提示
系統保持卸載項的引用,所以當它們從視圖層級結構中移除時,它們沒有被釋放。
4.2.1 最終(Final)和基礎(Base)尺寸類
界面生成器識別九個不同的尺寸類。
其中四個是最終尺寸類:Compact-Compact,Compact-Regular,Regular-Compact和Regular-Regular。最終尺寸類表示在設備上顯示的實際尺寸類。
其余五個是基礎尺寸類:Compact-Any,Regular-Any,Any-Compact,Any-Regular和Any-Any。這些抽象尺寸類表示兩個或多個最終尺寸類。例如,安裝在Compact-Any尺寸類中的項,出現在Compact-Compact和Compact-Regular尺寸視圖中。
在更具體尺寸類中的設置會覆蓋更通用的尺寸類。另外,你必須為所有九個尺寸類提供沒有歧義的,可滿足的布局,包括基礎尺寸類。因此,最簡單的方法是從最通用的尺寸類到最具體的尺寸類。選擇應用程序的默認布局,并在Any-Any尺寸類中設計該布局。然后根據需要修改基礎或最終尺寸類。
4.2.2 使用尺寸類工具
使用界面生成器的尺寸類工具選擇當前編輯的尺寸類。該工具在編輯創建的頂部中心。默認情況下,界面生成器選擇Any-Any尺寸類。

點擊尺寸類工具切換到新的尺寸類。界面生成器彈出一個包括3 × 3網格尺寸類的彈出框視圖。在網格中移動鼠標,改變尺寸類。網格在頂部顯示選中的尺寸類名稱,在底部顯示尺寸類的描述(包括它影響的設備和方向)。它還在當前尺寸類影響的每一個尺寸類中顯示一個綠色的原點。

添加到畫布的任何視圖或約束只安裝在當前尺寸類。當刪除項時,行為根據在哪和如何刪除項變化。
- 從畫布或者文檔大綱中刪除項,會從整個工程中移除。
- 從畫布或者文檔大綱中Command-Deleting項,只會從當前尺寸類中卸載項。
- 當場景有多個尺寸類時,從畫布或文檔大綱之外的任何地方(例如,從尺寸檢查器中選中并刪除約束)刪除項,只會從當前尺寸類終卸載項。
- 如果只在Any-Any尺寸類中編輯,刪除項總是從項目中移除它。
如果你正在編輯Any-Any之外的任何尺寸類,界面生成器在編輯器底部藍色高亮顯示工具欄。
4.2.3 使用檢查器
你還可以在檢查器中修改具體尺寸類的設置。任何支持具體尺寸類的設置,都在檢查器中帶一個小的加號圖標顯示。

默認情況下,檢查器為Any-Any尺寸類設置值。點擊加號圖標添加一個新的尺寸類,來設置更具體尺寸類的值。為你想添加的尺寸類選擇寬度,然后選擇高度。

現在,檢查器在它自己的行顯示每一個尺寸類——Any-Any設置在第一行,更具體的尺寸類在下面列出。你可以單獨編輯每一行的值。

點擊行開頭的x圖標移除一個自定義尺寸類。
在界面生成器中使用尺寸類的更多信息,請參考“Size Classes Design Help”。
4.3 使用滾動視圖
使用滾動視圖時,你需要同時定義滾動視圖在它父視圖內的frame的尺寸和位置,以及滾動視圖內容區域的尺寸。所有這些特征都可以使用自動布局設置。
為了支持滾動視圖,系統根據約束放置的不同地方,來解釋約束。
- 滾動視圖和滾動視圖之外的對象之間的任何約束,連接到滾動視圖的frame,跟其它任何視圖一樣。
- 滾動視圖和它內容之間的約束,行為根據被約束的屬性改變:
- 滾動視圖的邊緣(edge)或頁邊留白(margin)和它內容之間的約束,連接到滾動視圖的內容區域。
- 高度,寬度或中心之間的約束,連接到滾動視圖的frame。
- 還可以使用滾動視圖的內容和滾動視圖之外的對象之間的約束,為滾動視圖的內容提供一個固定位置,讓內容懸浮在滾動視圖中。
對于最普遍的布局任務,如果使用虛擬視圖,或者分組布局滾動視圖的內容,邏輯會變得更簡單。使用界面生成器時,通用方法如下:
- 添加滾動視圖到場景中。
- 跟通常一樣,繪制約束定義滾動視圖的尺寸和位置。
- 添加視圖到滾動視圖中。設置視圖的Xcode specific label為Content View。
- 固定內容視圖的頂部,底部,開頭和結尾邊緣到滾動視圖相應的邊緣。現在,內容視圖定義了滾動視圖的內容區域。
記住
此時內容視圖沒有固定的尺寸。它可以拉伸和增大來適應你放置在里面的任何視圖和控件。
- (可選)設置內容視圖的寬度等于滾動視圖的寬度,來禁用水平滾動。現在,內容視圖水平填充滾動視圖。
- (可選)設置內容視圖的高度等于滾動視圖的高度,來禁用垂直滾動。現在,內容視圖垂直填充滾動視圖。
- 在內容視圖中布局滾動視圖的內容。跟通常一樣,使用約束在內容視圖中放置內容。
重要
你的布局必須完全定義內容視圖額尺寸(除了步驟5和6中定義的)。要想根據內容的固有尺寸設置高度,必須有一個完整的約束鏈,以及從內容視圖的頂部邊緣到底部邊緣的視圖拉伸。類似的,要想設置寬度,必須有一個完整的約束鏈,以及從內容視圖的開頭邊緣到結尾邊緣的視圖拉伸。
如果內容沒有固有內容尺寸,則必須添加適當的尺寸約束到內容視圖或者到內容。
當內容視圖比滾動視圖高時,滾動視圖啟用垂直滾動。當內容視圖比滾動視圖寬時,滾動視圖啟用水平滾動。否則,默認情況下禁用滾動。
4.4 使用自我調整尺寸的表格視圖單元格
在iOS中,可以使用自動布局定義表格視圖單元格的高度;但是,該特征默認是禁用的。
通常,單元格的高度由表格視圖代理的tableView:heightForRowAtIndexPath:方法決定。要啟用自我調整大小的表格視圖單元格,必須設置表格視圖的rowHeight屬性為UITableViewAutomaticDimension。還必須給estimatedRowHeight屬性分配一個值。當這兩個屬性都設置后,自動使用自動布局計算行的實際高度。
tableView.estimatedRowHeight = 85.0
tableView.rowHeight = UITableViewAutomaticDimension
接下來,在單元格的內容視圖中布局表格視圖單元格的內容。要想定義單元格的高度,需要一個完整的約束鏈和視圖(已經定義了高度)填充內容視圖的頂部邊緣和底部邊緣之間的區域。如果視圖有固有內容高度,系統使用這些值。如果沒有,你必須添加適當的高度約束到視圖或者內容視圖本身。

另外,嘗試讓預估的行高盡可能準確。系統根據這些預估值計算項(比如滾動欄)的高度。預估值越精確,用戶體檢更天衣無縫。
提示
使用表格視圖單元格時,你不能改變預定義內容的布局(例如,textLabel,detailTextLabel和imageView屬性)。
支持以下約束:
- 相對于單元格的內容視圖約束子視圖的位置。
- 相對于單元格的bounds約束子視圖的位置。
- 相對于預定義內容約束子視圖的位置。
4.5 改變約束
一個約束的改變是改變底層約束的數學表達式(如圖17-1)。可以在“Anatomy of a Constraint”中學習更多約束方程式。
圖17-1 約束方程式

以下所有動作都會改變一個或多個約束:
- 啟用或禁止一個約束。
- 改變約束的常量值。
- 改變約束的優先級。
- 從視圖層級結構中移除一個視圖。
其它改變,比如設置控件的屬性,或者修改視圖層級結構,也會改變約束。當改變發生時,系統調度一個推遲的布局過程(deferred layout pass)。
通常,你可以在任何時候做出這些改變。理想情況是,大部分約束在界面生成器中設置,或者通過代碼,在視圖控制器的初始化設置中創建(例如,在viewDidLoad中)。如果需要在運行時動態改變約束,最好在應用程序狀態變化是改變它們。例如,你想在按鈕點擊時改變約束,直接在按鈕的動作方法中做出改變。
你可能偶爾因為性能原因,需要批量改變。更多信息請參考“批量改變”。
4.5.1 推遲的布局過程
自動布局為不久的將來調度一個布局過程,而不是立即更新受影響視圖的frame。該推遲的過程更新布局的約束,然后為視圖層級結構的所有視圖計算frame。
可以通過調用setNeedsLayout方法或者setNeedsUpdateConstraints方法,調度自己的推遲的布局過程。
推遲的布局過程實際涉及視圖層級結構的兩個過程:
- 更新過程根據需要更新約束。
- 布局過程根據需要重新定位視圖的frame。
4.5.1.1 更新過程
系統遍歷視圖層級結構,并在所有視圖控制器上調用updateViewConstraints方法,在所有視圖上調用updateConstraints方法。你可以覆寫這些方法,來優化約束的改變(查看“批量改變”)。
4.5.1.2 布局過程
系統再次遍歷視圖層級結構,并在所有視圖控制器上調用viewWillLayoutSubviews方法,在所有視圖上調用layoutSubviews(在OS X上layout)。默認情況下,layoutSubviews方法使用自動布局引擎計算的矩形更新每個子視圖的frame。你可以覆寫這些方法來修改布局(查看“自定義布局”)。
4.5.2 批量改變
影響變化發生后,立即更新約束幾乎總是更干凈和容易。推遲這些改變到一個之后的方法會讓代碼復雜,更難理解。
然而,有些時候你可能基于性能原因,希望批量修改。只有在就地改變約束太慢,或者當視圖做了很多多余的改變時,才應該這么做。
要想批量改變,在持有約束的視圖上調用setNeedsUpdateConstraints方法,而不是直接做出改變。然后,覆寫視圖的updateConstraints方法,來修改受影響的約束。
提示
你的updateConstraints實現必須盡可能高效。不要禁用所有約束,然后啟用你需要的。相反,你的應用程序必須有些方式來追蹤你的約束,并在每個更新過程中驗證它們。只有變化的項需要改變。在每一個更新過程中,你必須確保應用程序的當前狀態有合適的約束。
總是在你實現的updateConstraints方法的最后一步調用父類的實現。
不要在你的updateConstraints方法中調用setNeedsUpdateConstraints。調用setNeedsUpdateConstraints調度另一個更新過程,創建了一個反饋回路(feedback loop)。
4.5.3 自定義布局
覆寫viewWillLayoutSubviews或layoutSubviews方法來修改布局引擎返回的結果。
重要
如果可能,使用約束定義所有布局。結果布局更健壯和更容易調試。當你需要創建的布局不能只使用約束表示時,你應該只覆寫viewWillLayoutSubviews或layoutSubviews方法。
覆寫這些方法時,布局在一個不一致的狀態。有些視圖已經布局好了,其它的還沒有。你需要十分小心如何修改視圖層級結構,否則會創建反饋回路。以下規則幫助你避免反饋回路:
- 必須在你的方法中某些地方調用父類的實現。
- 你可以安全的在你的子樹(subtree)中讓視圖的布局無效;但是,必須在調用父類的實現之前。
- 不要在你的子樹之外讓任何視圖的布局無效。這會創建一個反饋回路。
- 不要調用setNeedsUpdateConstraints。你剛完成一個布局過程。調用該方法會創建一個反饋回路。
- 不要調用setNeedsLayout。調用該方法會創建一個反饋回路。
- 小心的改變約束。你不想在子樹之外讓任何視圖的布局意外的無效。