Swift. Auto Layout (5)

讓我們來看看 Auto Layout 中常見的錯誤

Jeremy Xue
Jeremy Xue ‘s Blog
22 min readJul 15, 2021

--

Photo by UX Store on Unsplash

▸ Auto Layout 錯誤

Auto Layout 中的錯誤可以被分為三大類:

  • 無法滿足的佈局(Unsatisfiable Layouts):你的佈局沒有有效的解決方案。
  • 模糊的佈局(Ambiguous Layouts):你的佈局有兩個或更多的解決方案。
  • 邏輯錯誤(Logical Errors):你的佈局邏輯存在著錯誤。

大多數的時候,真正問題只是確認發生了什麼錯誤。你添加你認為需要的約束,但當你運行應用時卻沒有如你所願。

通常,一旦你瞭解問題後,解決方案就顯而易見了。可能是刪除衝突的約束、添加遺漏的約束、調整比較優先度。當然,達到可以輕鬆理解問題的程度可能需要透過反覆試驗來達到。就像任何技能一樣,熟能生巧。

▸ 無法滿足的佈局

識別無法滿足的約束:

當系統無法為當前的約束找到有效的解決方案時,就會產生無法滿足的佈局。兩個或是更多的必要約束發生衝突,因為他們無法同時符合。

當系統在運行時檢測到不可滿足的佈局時,他會執行以下步驟:

  1. Auto Layout 識別一組衝突的約束
  2. 它取消了衝突的其中一個約束並且檢查佈局,系統會繼續取消約束,直到找到有效的佈局。
  3. Auto Layout 將有關衝突和取消約束的訊息記錄到控制台。

此後備系統讓應用程式繼續運行,同時仍然向使用者呈現一些有意義的東西。但是,取消約束的效果可能因佈局而異,甚至因為建構而異。

許多情況下,缺少約束可能沒有可見的影響。畫面會與你預期相同。但在其他的狀況下,缺少約束可能會導致畫面的部分位置、尺寸錯誤或是完全消失。

當錯誤沒有明顯的影響時,通常很容易忽略錯誤,畢竟並不會改變應用程式的行為。但是,對畫面或 SDK 的任何更改也可能導致一組約束錯誤,突然產生明顯錯誤的佈局。

因此,在檢測到不可滿足的錯誤時,請務必修正他們。為確保你在測試期間捕獲不明顯的錯誤,請為 UIViewAlertForUnsatisfiableConstraints 設置一個中斷點。

防止無法滿足的約束

無法滿足的約束相對容易修復,系統會在無法滿足的約束出現時告訴你,並提供衝突約束的列表。

一旦你知道錯誤,解決方法通常非常簡單。刪除其中一個約束或將其改為可選約束。

但是,有些常見的問題值得詳細研究:

以程式碼將 View 添加到畫面時,經常會出現不可滿足的約束。

默認情況下,View 的 translatesAutoresizingMaskIntoConstraints 屬性設置為 true,當你開始為畫面中的 View 設置約束時,Interface Builder 會自動將此屬性設置為 false。但是如果你是以程式碼方式創建佈局時,你需要在添加自訂義約束之前將此屬性設置為 false。

當畫面呈現在對於它來說太小的空間時,經常會發生無法滿足的約束。

你通常可以預測你的 View 可以訪問到的最小空間值,並適當的設計你的佈局。然後,國際化(本地化)和動態類型都會導致 View 比預期的內容大很多。隨著可能的排列數量增加,越來越難以保證你的佈局在所有狀況下都能正常作用。

相反的,你可能想要建立故障點,以便於你的佈局可預測、以可控的方式失敗。考慮一些必須的約束轉換為高優先度的可選約束,這些約束使你可以控制衝突發生時佈局所中斷的位置。.

例如,將故障點的優先度設置為 999。在大多數狀況下,此優先度的約束行為就類似於必須的;但是,當衝突發生時,高優先度的約束會被取消,從而保護佈局中剩餘的其他部分。

同樣的,避免具有固有內容尺寸的 View 提供所需的 content-hugging 或 compression-resistance 優先度。通常,控制元件的尺寸可以作為理想的故障點,控制元件可以大一點或小一點,而不會對佈局產生任何有意義的影響。
當然,有些控制元件應該只在其內容尺寸中顯示。但是,即使在這些情況下,通常最好有一個控制元件偏離幾個大小,而不是讓你的佈局以無法預測的方式中斷。

▸ 模糊的佈局

當約束系統有兩個或多個有效的解決方案時,就會出現模糊的佈局,主要有兩個原因:

  • 佈局需要額外的約束來唯一指定每個 View 的位置。在確定哪些 View 不明確後,只需添加約為以唯一指定 View 的位置和尺寸。
  • 佈局在具有相同優先度的衝突可選約束,系統不知道應該取消哪個約束。在這邊你需要透過修改優先度來使它們不再相等來告訴他應該取消哪個約束,系統首先取消最地優先度的約束。

偵測模糊的佈局

與無法滿足的佈局相同,Interface Builder 通常可以在設計時偵測並且提供修復模糊佈局的建議,這些歧義顯示為問題導航列(issues navigator)中的警告,文檔大綱中的錯誤以及畫布中的紅線。但 Interface Builder 無法偵測所有可能的歧異,許多錯誤只有透過測試才能發現。

當運行出現模糊的佈局時,Auto Layout 會選擇一種可能的解決方案來使用。這意味著佈局可能會或可能不會如你預期。此外,警告並不會寫入控制台也無法為模糊的佈局設置斷點。

因此,與無法滿足的佈局相比,模糊的佈局通常難以偵測和識別,即使歧異具有明顯可見的效果,也很難確定錯誤是由於奇異還是佈局邏輯中的錯誤所造成的。

幸運的是,你可以調用一些方法來幫助你識別模糊的佈局。所有這些方法都應該只適用於 debug。可以在訪問 View Hierarchy 的某處設置中斷點,然後從控制台調用以下方法之一:

  • hasAmbiguousLayout :適用於 iOS 和 OS X 兩者。在錯位的 View 上調用此方法,如果 View 的 frame 不明確,則返回 YES,否則,則為 NO。
  • exerciseAmbiguityInLayout:適用於 iOS 和 OS X 兩者。在模糊的佈局的 View 中調用此方法,這將在可能的有效解決方案之間切換系統。
  • constraintsAffectingLayoutForAxis :適用於 iOS。在 View 上調用此方法。它將返回影響該 View 的所有約束的數組。
  • constraintsAffectingLayoutForOrientation:適用於 OS X。在 View 上調用此方法。它將返回影響該 View 的所有約束的數組。
  • _autolayoutTrace :在 iOS 中作為私有方法。在 View 上調用此方法,它將返回一個字串,它返回關於包含該視圖的整個 View Hierarchy 的診斷訊息字串。模糊的 View 將被標記,並且將 translatesAutoresizingMaskIntoConstraints 設為 YES 的 View 也會被標記。

在控制台執行這些指令時,你可能需要使用 Objective-C 語法。例如,在中斷點停止執行後,在控制台中輸入 call [self.myView exerciseAmbiguityInLayout] 來調用 myView 上的 exerciseAmbiguityInLayout 方法。同樣的,輸入 po [self.myView.autoLayoutTrace] 來印出有關包含 myView 的 View Hierarchy 的診斷訊息。

Note在運行上面所列出的診斷方法之前,請務必修復 Interface Builder 上發現的任何問題。Interface Builder 嘗試修正任何所發現的錯誤,這意味著如果他發現一個模糊的佈局,它會添加約束使其佈局不再有歧異。因此,當 hasAmbiguousLayout 返回 NO,exerciseAmbiguityInLayout 似乎沒有任何效果,constraintsAffectingLayoutForAxis 可能會返回額外或意外的約束。

▸ 邏輯錯誤

邏輯錯誤單純只是個 bug。在某處,你有一個錯誤的假設,也許這是關於 Auot Layout 如何計算 View frame 的假設,也許這是對你創建的約束或你設置的 View 屬性的假設。或許這是關於約束如何交互作用來創建複雜行為的假設。無論如何,某些東西可能與你的心智模型不太符合。

邏輯錯誤是最難發現的,在排除其他所有可能性後,剩下的無論多麼不可能,一定都是邏輯錯誤。但是,即使你確定存在錯誤之後,你仍然必須發現錯誤假設的確切位置。

這裡沒有工具或一步步的指示。修正邏輯錯誤通常涉及實驗和迭代測試來識別問題並找出如何修復的方式。但是,有一些建議可能有所幫助:

  • 查看現有約束,確保你沒有缺少任何約束或是意外的添加不需要的約束,確保所有約束都附加到正確的項目以及屬性上。
  • 仔細檢查 View frame。確保沒有任何洞悉意外被拉伸或是縮小。這對於具有透明背景的 View 尤其重要,例如 Label 和 Button,當這些項目意外調整尺寸時,可能不明顯。
    調整尺寸的其中一種症狀是 baseline 對其的 View 不再正確排列,這是因為 baseline 對齊只在 View 以固有內容高度顯示時才有作用,如果是垂直拉伸或縮小 View,文字會神秘的出現在錯誤的位置上。
  • 如果控制元件始終與固有內容尺寸匹配,則為其提供非常高的 content-hugging 和 compression-resistance 優先度(例如 999)。
  • 尋找你對佈局所做的任何假設,並添加明確的約束來確保這些假設是正確的。請記住,無法滿足的佈局通常是最容易發現和修復的,添加其他約束,直到出現衝突,然後檢查並且修正衝突。
  • 嘗試理解為什麼給定的約束會產生你所看見的畫面,如果你越理解它,你就能夠很好的修正它。
  • 嘗試著替代約束,Auto Layout 通常為同一個問題提供多種不同的解決方案,嘗試不同的方案可能會解決問題,或者至少可以更容易地發現錯誤。

▸ 了解控制台的 Log

關於 View 的相關訊息會在控制台被印出,因為存在著無法滿足的佈局,或者因為你使用了 constraintsAffectingLayoutForAxis:constraintsAffectingLayoutForOrientation: debug 方法來明確記錄了約束。

無論哪種方式,你都可以在這些 log 中找到很多很用的資訊。以下是無法滿足的佈局錯誤的範例輸出:

2015-08-26 14:27:54.790 Auto Layout Cookbook[10040:1906606] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7a87b000 H:[UILabel:0x7a8724b0'Name'(>=400)]>",
"<NSLayoutConstraint:0x7a895e30 UILabel:0x7a8724b0'Name'.leading == UIView:0x7a887ee0.leadingMargin>",
"<NSLayoutConstraint:0x7a886d20 H:[UILabel:0x7a8724b0'Name']-(NSSpace(8))-[UITextField:0x7a88cff0]>",
"<NSLayoutConstraint:0x7a87b2e0 UITextField:0x7a88cff0.trailing == UIView:0x7a887ee0.trailingMargin>",
"<NSLayoutConstraint:0x7ac7c430 'UIView-Encapsulated-Layout-Width' H:[UIView:0x7a887ee0(320)]>"
)

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7a87b000 H:[UILabel:0x7a8724b0'Name'(>=400)]>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

此錯誤訊息展示了五個衝突的約束,並非所有這些約束可以同時成立。你需要刪除一個或者將其轉換為可選約束。

幸運的是 View Hierarchy 相對簡單。你有一個包含 Label 與 TextFiled 的 Superview,衝突的約束設置了以下關係:

  • Label 的 width 大於或等於 400 點
  • Label 的 leading 等於 Superview 的 leading
  • Label 與 TextField 之間有 8 點的間距
  • TextField 的 trailing 等於 Superview 的 trailing
  • Superview 的 width 等於 320 點

系統嘗試取消 Label 的 width 來復原。

NOTE約束使用可視化語言(Visual Format Language)被寫入控制台。即使你從未使用過可視化語言方式來創建自己的約束,你也必須能夠閱讀和理解它才能夠有效的 debug Auto Layout 問題。有關更多資訊,請參考 Visual Format Language

在這些約束中,最後一個是由系統所創建的,你無法改變它。此外,它與第一個約束產生明顯的衝突,如果你的 Superview.width 只有 320 點,你永遠不可能擁有一個 width 400 點的 Label。幸運的是,你不必擺脫第一個約束,如果你先將其優先度降低至 999,系統仍然會嘗試提供選定的寬度,在滿足其他約束的同時盡可能的接近。

基於 View 的 autoresizing mask(例如,當 translatesAutoresizingMaskIntoConstraints 為 YES 時所創建的約束)具有 mask 的額外資訊。在約束記憶體位置之後,log 字串顯示 h= 後跟著 3 個字符,v= 後跟著三個字符,- 表示固定值,而 & 表示彈性值。對於 horizontal mask(h=)這三個字符表示 left margin、width 和 right margin。而對於 vertical mask(v=)這三個字符表示 top margin、height、bottom margin。

例如以下 log 訊息:

<NSAutoresizingMaskLayoutConstraint:0x7ff28252e480 h=--& v=--& H:[UIView:0x7ff282617cc0(50)]>"

此 log 訊息由以下內容所組成:

  • NSAutoresizingMaskLayoutConstraint:0x7ff28252e480 :約束的 class 與記憶體位置,在此範例中,此 class 告訴你它是基於 View 的 autoresizing mask
  • h=--& v=—& :View 的 autoresizing mask。在這是 default mask。在水平方向中,它有一個固定的 left margin、一個固定的 width 以及彈性的 right margin。在垂直方向中,它有一個固定的 top margin、一個固定的 height 以及一個彈性的 bottom margin。換句話說,當 Superview 的尺寸改變時,View 的左上角以及尺寸保持不變。
  • H:[UIView:0x7ff282617cc0(50)] :約束的可視化語言描述。此範例中,它定義了一個寬度為 50 點的 View,此描述還包含了受約束影響的任何 View 的 class 與記憶體位置。

▸ 添加標識符到 Log 中

儘管前面的範例相對容易理解,但較長的約束列表很容易變得難以理解。你可以透過每個 View 和約束提供一個有意義的標識符來使 Log 更容易閱讀。

如果 View 有明顯的文字元件,Xcode 會使用它作為標識符。例如,Xcode 使用的 Label 的 text、Button 的 title 或是 TextField 的 placeholder 來標識這些 View。不然得話就需要在身份檢查器(Identity inspector)中設置 View 的 Xcode 特定標籤。Interface Builder 在其整個介面中使用這些標識符,其中許多內容還顯示在控制台 log 中。

對於約束,以程式碼方式或是使用屬性檢查器設置標識符屬性,接著 Auto Layout 將在訊息印出到控制台之前使用這些標識符。

以下是標識符設置相同的無法滿足的約束錯誤:

2015-08-26 14:29:32.870 Auto Layout Cookbook[10208:1918826] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(
"<NSLayoutConstraint:0x7b58bac0 'Label Leading' UILabel:0x7b58b040'Name'.leading == UIView:0x7b590790.leadingMargin>",
"<NSLayoutConstraint:0x7b56d020 'Label Width' H:[UILabel:0x7b58b040'Name'(>=400)]>",
"<NSLayoutConstraint:0x7b58baf0 'Space Between Controls' H:[UILabel:0x7b58b040'Name']-(NSSpace(8))-[UITextField:0x7b589490]>",
"<NSLayoutConstraint:0x7b51cb10 'Text Field Trailing' UITextField:0x7b589490.trailing == UIView:0x7b590790.trailingMargin>",
"<NSLayoutConstraint:0x7b0758c0 'UIView-Encapsulated-Layout-Width' H:[UIView:0x7b590790(320)]>"
)

Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x7b56d020 'Label Width' H:[UILabel:0x7b58b040'Name'(>=400)]>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

如你所見,這些標識符可以使你更快速且容易的辨認 log 中的約束。

▸ 可視化 View 和約束

Xcode 提供的工具可幫助你可視化 View Hierarchy 中的 View 和約束。要使用模擬器中查看這些內容,可以依照以下步驟:

  1. 在模擬器中運行應用程序
  2. 切回 Xcode
  3. 工具列選擇 Debug > View Debugging > Show Alignment Rectangles,設置概述了 View 的邊緣。

對齊矩形(alignment rectangles)是 Auto Layout 使用的邊緣。開啟此選項可以使你快速發現任何意外調整的尺寸的對齊矩形。

如果你還需要更多訊息,請點擊 Debug View Hierarchy 按鈕。

Debug View Hierarchy 按鈕圖示

Xcode 將會顯示一個交互式的 View Debugger 為你提供許多用於探索與交互 View Hierarchy 的工具。在 debug Auto Layout 時,Show clipped content 與 Show constraints 特別有用。

啟用 Show clipped content 會顯示可能意外放置在畫面外 View 的位置。而啟用 Show constraints 會顯示影響當前選定 View 的所有約束。當畫面開始出現異常時,這兩個功能都提供了快速的全面性檢查。

關於更多資訊,可以查看 Debug Area Help

▸ 了解邊緣狀況

以下是一些可能導致 Auot Layout 以意外方式運行的邊緣狀況:

  • Auto Layout 根據他們的對齊矩形而不是他們的 frame 來定位 View。大多數的情況下,這些值是相同的。但是,某些 View 可能會設置自定義的對齊矩形從佈局的計算中排序部份 View(例如:徽章)。更多訊息可以查看 UIView 文件中的 Aligning Views with Auto Layout 章節。
  • 在 iOS 中,你可以使用 View 的 transform 屬性來調整尺寸、旋轉、位移 View。然而,這些轉換不會以任何方式影響 Auto Layout 的計算。Auto Layout 是根據未轉換的 frame 計算 View 的對齊矩形。
  • View 可以在其邊界之外顯示內容。大多數狀況下,View 行為正常並且將其內容限制於其邊界。然而,出於性能因素,這不是由圖像引擎強制執行的。這意味著 View(尤其是具有自定義繪圖的 View)可能與其 frame 不同的尺寸所繪製。你可以透過將 View 的 clipsToBounds 屬性設置為 YES 來驗證 View frame 的尺寸來識別者些錯誤。
  • NSLayoutAttributeBaseline、NSLayoutAttributeFirstBaseline 和NSLayoutAttributeLastBaseline 屬性只有在所有 View 以固有內容高度顯示時才能正確對齊文字,如果其中一個 View 被垂直拉伸或是收縮,其文本可能會出現在錯誤的位置。
  • 約束優先度作為整個 View Hierarchy 的全局屬性,你通常可以透過 StackView、Layout Guide 或 Dummy View 的方式來對 View 進行分組。但是,這種方法並沒有封裝包含 View 的優先度,Auto Layout 會繼續比較組內的優先度與組外的優先度(甚至其他組中的優先度)。
  • 長寬比(Aspect ratio)約束允許水平與垂直的約束相互作用。通常,水平和垂直佈局是分開計算的。但是,如果你相對於其寬度來約束 View 的高度,那麼你就在水平與垂直約束之間建立了連結,他們現在可以互相影響和衝突。這種交互方式極大的增加了佈局的複雜度,並且可能導致佈局不相關的部分出現意外的衝突。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]