投稿文章,作者:一縷殇流化隱半邊冰霜(@halfrost)
目錄
1.Incrementally Adopting Auto Layout
2.Design and Runtime Constraints
3.NSGridView
4.Layout Feedback Loop Debugging
一.Incrementally Adopting Auto Layout
Incrementally Adopting Auto Layout是什麼意思呢?在我們IB裡面布局我們的View的時候,我們並不需要一次性就添加好所有的constraints。我們可以一步步的增加constraints,簡化我們的步驟,而且能讓我們的設置起來更加靈活。
再談新特性之前,先介紹一下這個特性對應的背景來源。
有這樣一種場景,試想,我們把一個view放在父view上,這個時候並沒有設置constraints,當我們運行完程序,就會出現下圖的樣子。
看上去一切都還正常。但是一旦當我們把設備旋轉90°以後,就會出現下圖的樣子。
這個時候可以發現,這個View的長,寬,以及top和left的邊距都沒有發生變化。這時我們並沒有設置constraints,這是怎麼做到的呢?
在程序的編譯期,Auto Layout的引擎會自動隱式的給View加上一些constraints約束,以保證View的大小不會發生變化。這個例子中,View被加上了top,left,width,height這4個約束。
如果我們需要更加動態的resize的行為,就需要我們在IB裡面自定義約束了。現在問題就來了,有沒有更好的方式來做這件事情?最好是能有一種不用約束的方法,也能達到簡單的resize的效果。
現在這個問題有了解決辦法。在Xcode 8中,我們可以給View指定autoresizing masks,而不用去設置constraints。這就意味著我們可以不用約束,我們也能做到簡單的resize的效果。
在Autolayout時代之前,可能會有人認出這種UI方式。這是一種Springs & Struts的UI。我們可以設定邊緣約束(注:這裡的約束並不是指的是Autolayout裡面的constraints,是autoresizing masks裡面的規則),無論View的長寬如何變化,這些View都會跟隨著設置了約束的view一起變化。
上述的例子中,Xcode 8 中在沒有加如何constraint就可以做到旋轉屏幕之後,View的邊距並沒有發生變化。這是怎麼做到的呢?事實上,Xcode 8的做法是先取出autoresizing masks,然後把它轉換成對應的constraints,這個轉換的時機發生在Runtime期間。生成對應的constraints是發生在運行時,而不是編譯時的原因是可以給我們開發者更加便利的方式為View添加更加細致的約束。
在View上,我們可以設置translatesAutoresizingMaskIntoConstraints屬性。
translatesAutoresizingMaskIntoConstraints == true
假設如果View已經在Interface Builder裡面加過constraints,“Show the Size inspector”面板依舊會和以前一樣。點擊View,查看給它加的所有的constraints,這個時候Autoresizing masks就被忽略了,而且translatesAutoresizingMask的屬性也會變成false。如下圖,我們這個時候在“Show the Size inspector”面板上面就已經看不到AutoresizingMask的設置面板了。
上圖就是在Autolayout時代之前,我們一直使用的是autoresizing masks,但是Autolayout時代來臨之後,一旦勾選上了這個Autolayout,之前的AutoresizingMask也就失效了。
回到我們最原始的問題上來,Xcode 8 現在針對View可以支持增量的適用Autolayout。這就意味著我們可以從AutoresizingMask開始,先做簡單的resize的工作,然後如果有更加復雜的需求,我們再加上適當的約束constraints來進行適配。簡而概之,Xcode 8 Autolayout ≈ AutoresizingMask + Autolayout 。
接下來用一個demo的例子來說明一下Xcode 8 Autolayout新特性。
在說例子之前我們先來說一下Xcode 8在storyboard上新增了哪些功能。如下圖,我們可以看到,在最下方新增加了一欄,可以切換不同的屏幕大小,可以看出,iPhone現在已經分化成6種屏幕大小需要我們適配了,從大到小,依次是:iPad pro 12.9, iPad 9.7 , iPhone 6s Plus/iPhone 6 Plus , iPhone 6s/iPhone 6, iPhone SE/iPhone5s/iPhone5, iPhone4s/iPhone4。下面還可以選擇橫豎屏,和不用屏幕百分比的適應性。
回到例子,我們現在對頁面上這些view來做簡單的AutoresizingMask。右邊的那個預覽界面是可以看到我們加上這些Mask之後的效果。
先是粉色的父View,我們給它加上如下的AutoresizingMask。
給"雨天"的imageView加上如下AutoresizingMask
給"陰天"的imageView加上如下的AutoresizingMask
最後給我們的中間的Label加上AutoresizingMask
這個時候我們旋轉一下屏幕,一切正常,View的排版都如我們所願。
這個時候我們再選擇一下,3:2分屏,這個時候就出現了不對的情況了。Label的Width被擠壓了。
原因是因為Autoresizing masks並不會向Autolayout一樣,會考慮View的content,所以這裡被擠壓了。
想fix這個Label,我們可以很容易的添加一個constraints來修復。不過這裡我們來談談另外一種做法。
進入到Attributes Inspector面板,找到Autoshrink屬性,把“fixed font size”切換成“minimum font size”
這個時候就fix上述的問題了。
此時就算是回到landscape,分屏的情況下,已經可以顯示正常。
接著我們再來處理一下中間的溫度的Label。這個時候我們有比較復雜的需求。這個時候我們就需要用到constraint了。
這個時候我們按時control鍵,然後拖到父View上,釋放,會彈出菜單。我們再按住shift,這樣我們可以一次性選擇多個constraints。
我們一次性選擇“Center Horizontally in Container” 和 “Center Vertically in Container”。注意這個時候右邊還是AutoresizingMask的面板,因為這個時候Label還沒有任何的constraint。當我們點擊“Add Constraints”的時候,就給Label加上了約束,右邊的面板也變成了constraints面板了。
我們再給這個Label繼續加2個constraints。“Horizontal Spacing”和“Baseline”。
同樣的,從Label拖拽到“太陽”的那個imageView上,再添加“Horizontal Spacing”和“Baseline”約束。
這個時候我們更新一下frame。如下圖所示,選擇“Update Frames”,這個時候所有的frame就都完成了。
這個時候我們更新一下中間溫度的Label的字體大小,這時候計算變大,由於我們的constraints都是正確的,兩邊的View也會隨著Label字體變大而變大。
Xocde 8在這個時候就變得更加智能了,會立即自動更新frame。
我們在繼續給晴天的上海加上一個背景圖。添加一個imageView,然後大小鋪滿整個父View,把mode 選擇成“Aspect Fill”
接下面一般的做法就是在這個imageView上面添加constraints,來使這個View和父View大小一樣。但是這種簡單的resize的行為在Xocde 8裡面就不需要再添加Constraint了,這裡我們改用Autoresizing masks來實現。給imageView添加一下這些mask。
我們把imageView放到背景去。這時,我們所有的界面就布局完成了。
測試一下橫屏的效果
甚至分屏的一樣可以完成任務!
Demo的Github地址,這個demo沒啥難的,就是看看效果。
這就是Xcode 8 的Incrementally Adopting Auto Layout,Autoresizing masks + Auto Layout Constraint 一起協同工作!
二.Design and Runtime Constraints
在我們開發過程中有這樣一種情況,View的constraints會依據你所加載的數據來添加的。所以在app運行之前,我們是無法知道所有的constraints的。
這裡有3種方法可以對應以上的情況。
1.Placeholder Constraints
假設現在我們需要把一張圖片放在View的垂直和水平的中間,並且距離左邊的邊緣有一個leading margin。並且還需要保持其長寬的比例。而這種圖片的最終樣子,我們並不知道。只有到運行時,我們才能知道這樣圖片的樣子。
為了能在Interface Builder看到我們的圖片,我們要先預估一下圖片的長寬比例。假設我們估計為4:3。這時候就給圖片加上constraints,並且勾上“place order constraint”,這個約束會在build time的時候被移除。
當我們在運行時拿到圖片之後,這個是時候我們再給它加上適當的約束和長寬比例即可。
2.Intrinsic Content Size
還是類似上面那種場景,我們有時候會自定義一些UIView或者NSView,這些View裡面的content是動態的。Interface Builder並不會運行我們的代碼,所以不到app運行的時候我們並不知道裡面的大小。我們可以給它設置一個內在的content的大小。
Setting a design time intrinsic content size only affects a view while editing in Interface Builder.The view will not have this intrinsic content size at runtime.
注意一下上面的說明intrinsic content size僅僅相當於是在布局的時候一個placeholder,在運行時這個size就沒有了。所以如果開發過程中真的需要用到這個內在的content的大小,那麼我們需要overriding的content size
override var intrinsicContentSize: CGSize
3.Turn Off Ambiguity Per View
這個是Xcode 8的一個新特性。當上述2種方法都無法解決我們的需求的時候。這個時候就需要用到這種方法了。Xcode 8給了我們可以在constraints產生歧義的時候,可以動態調整警告級別的能力。
在這個場景中,我們僅僅只知道我們需要把這個imageView放在水平位置的中央,但是imageView的大小和它的水平位置我們並不知道。如果我們僅僅只加上了這一個約束的話,Interface Builder就會報紅,因為IB這時候根據我們給的constraints,並不能唯一確定當前的view的位置。
如果我們在之後的運行時,拿到圖片的完整信息之後,我們自己知道該如何去加constraints,我們知道該如何去排版保證imageView能唯一確定位置的時候,這時我們可以關掉IB的紅色警告。找到“Ambiguous”,這裡是警告的級別,我們這裡選擇“Never Verify”,這時就沒有紅色的警告和錯誤提醒了。但是選擇這一項的前提是,我們能保證之後運行時我們可以加上足夠的constraints保證view的位置信息完整。
以上3種方法就是我們在運行時給view增加constraints的解決辦法。
三.NSGridView
這是macOS給我們帶來的一個新的layout容器。
有時候我們為了維護constraints的正確性是件比較麻煩的事情,比如即使我們就是一組簡單的checkboxes,維護constraints也不容易。這個時候我們會選擇用stack view來讓我們開發更容易一些。
下圖是macOS的app常見到的一組checkboxes。
這時候我們選用NS/UIStackView來實現,因為它有以下的優點,它可以排列一組items,重要的是它可以處理好content size並且可以控制好每個item之間的spacing。
但是stack view依舊有一些場景無法很順手的處理。例如下圖的場景。
這時依舊可以用stack view來實現,但是它不能幫我們根據content完成行和列的對齊。
這就是為什麼要引入新的NSGridView的原因。
使用NSGridView,我們可以很容易的做到content在X軸和Y軸上的對齊。僅僅只需要我們把content放進預先定義好的網格中即可,NSGridView會幫我們管理好接下來對齊的一切事情。
我們來看看下面的例子。
NSGridView有2個子類,NSGridRow 和 NSGridColumn,它們倆會自動的管理好content的大小。當然我們可以在需要的時候指定size的大小,padding和spacing的大小。我們也可以動態的隱藏一些rows行和colunms列。
NSGridCell的工作就是管理每個cell裡面content view的layout。如果某個cell的內容超出cell的邊界,cell會合並起來,就像普通的電子表格app的做法一樣。
我們來構建一個簡單的界面。設計圖如下:
我們並不需要去關心網格的sizing,我們只用關心每一行每一列究竟有多少個content需要被顯示出來。
let empty = NSGridCell.emptyContentView let gridView = NSGridView(views: [ [brailleTranslationLabel, brailleTranslationPopup], [empty, showContractedCheckbox], [empty, showEightDotCheckbox], [statusCellsLabel, showGeneralDisplayCB], [empty, textStyleCB], [showAlertCB] ])
用上述代碼運行出來的界面是這樣的:
雖然我們調用構造函數沒錯,但是出來的界面和設計的明顯有一些差距。最明顯的問題就是UI被拉開了,有很多空白的地方。
產生問題的原因就在於,網格被約束到了window的邊緣。我們的意圖應該是window來匹配我們的網格大小,但是現在出現的問題變成了,網格被拉伸了,去匹配window的大小了。
我們解決這個問題的辦法就是去改變 grid view內容的hugging的優先級。盡管頁面上的constraints已經具有了高優先級,但是我們現在仍可以繼續提高優先級,來讓constraints推動content,使其遠離window的邊緣。我們提高一些優先級:
gridView.setContentHuggingPriority(600, for: .horizontal) gridView.setContentHuggingPriority(600, for: .vertical)
我們會發現,window裡面的content更加聚合了,中間的大段空白消失了。
我們再來解決一下window中間的空白,左邊的label和右邊的content距離太遠。根據設計,我們應該讓label居右排列。這件事很容易,只要我們調整一下cell的位置信息即可完成。排列的位置信息會影響到cell,行,列,網格視圖。
如果沒有指定cell的placement這個屬性值,那麼行列就會根據gridview的placement屬性值來確定。這個規則可以使我們在一處設定好placement,瞬間可以改變大量的cell的布局。
//first column needs to be right-justified: gridView.column(at: 0).xPlacement = .trailing
我們找到gridView的第一列,改變它的xPlacement屬性值,這樣一列的cell都會變成居右排列。
居右之後,我們又會出現新的問題,baseline不對齊了。
行的對齊和列的對齊原理一樣的,同理,我們只需要設置一處,將會影響整個網格視圖。
// all cells use firstBaseline alignment gridView.rowAlignment = .firstBaseline
設置完成之後,整個網格視圖就對齊了。
接下來我們再來改變一下pop-up button的邊距。
let row = gridView.cell(for: brailleTranslationPopup)!.row! row.topPadding = 5 row.bottomPadding = 5
這裡取第一行的做法也可以和之前取第一列的做法一樣,直接取下標0的row即可。這裡換一種更好的做法來做。在gridView裡面找到包含pop-up button的cell,根據cell找到對應的row行。這種方式比直接去下標index的好處在於,日後如果有人在index 0的位置又增加了一行,那麼代碼就出錯了,而我們這裡的代碼一直都不會出錯,因為保證是取出了包含pop-up button的cell。所以代碼裡面盡量不要寫死固定的index,這樣以後維護起來比較困難。
同理,我們也給“status cells”也一起加上Padding
ridView.cell(for:statusCellsLabel)!.row!.topPadding = 6
這裡需要對比一下padding 和 spacing的區別。
padding是針對每個行或者每個列之間的間距,我們可以增加padding來改變兩兩之間的間距。
spacing是針對整個gridview來說的,改變了它,將會影響整個網格視圖的布局。
再來看看我們的設計圖:
如果沒有padding那麼就是下圖的樣子:
如果沒有spacing那麼就會出現下圖的樣子:
如果spacing和padding都沒有的話,那就都擠在一起了:
最後我們來處理一下最下面那一行包含checkbox的cell
這裡就需要用到之前提到了,合並2個cell了。
// Special treatment for centered checkbox: let cell = gridView.cell(for: showAlertCB)! cell.row!.topPadding = 4 cell.row!.mergeCells(in: NSMakeRange(0, 2))
這裡我們直接指出了,合並前2個cell。
執行完代碼之後,就會是這個樣子。
最後一行的cell就會橫跨2個cell的位置。雖然占了2個cell的位置,但是它依舊還繼承著第一列的居右的排列規則。
現在我們的需求是既不希望它居右,也不希望它居左。
checkbox其實是支持排列在2個列之間的,但是由於這相鄰的2個列的寬度並不相等,所以gridview不知道該怎麼排列了。這時就需要我們手動來改變布局了。
這裡可能有人會想,直接把
cell.xPlacement = .none
把cell的xPlacement直接變成none,這樣做會一下子打亂整個gridview的constraints布局,我們不能這樣做。我們需要再繼續給cell加上額外的constraints來維護整個gridview的constraints的平衡。
cell.xPlacement = .none let centering = showAlertCB.centerXAnchor.constraint(equalTo: textStyleCB.leadingAnchor) cell.customPlacementConstraints = [centering]
我們只需要在給出checkbox在x軸方面的錨點即可。這時候checkbox就會排列成我們想要的樣子了。
至此,我們就完成了需求。總結一下,NSGridView是一個新的控件,能很好的幫助我們進行網格似布局。它能很快很方便的把我們需要展示的content排列整齊。之後我們僅僅只需要調整一下padding和spacing這些信息即可。
四.Layout Feedback Loop Debugging
有時候我們設置好了constraint之後,沒有報任何錯誤,但是有些情況當我們運行起來的時候就有一堆constraint沖突在debug窗口裡面,嚴重的還會使app直接崩潰。崩潰的情況就是遇到了layout feedback loop。
遇到這種情況,往往是發生在“過渡期”,開始或者結束的時候。如果說你點擊了一個button,button相應了你的點擊,但是之後button不彈起,一直保持著被按下的狀態。
然後會觀察到CPU使用率爆表,內存倍增,然後app就崩潰了,與此同時返回了一大堆的layout的棧回溯信息。
發生這個情況的原因是某個view的layout被一直執行,一直執行,陷入了死循環中。Runloop就不會停下,CPU的使用率會一直處於峰值。所有的消息都會被收集到自動釋放的對象中去,消息一直發送,就會一直收集。所以內存也會倍增。
導致這個原因之一,是setNeedsLayout這個方法。
當其中一個view調用完setNeedsLayout之後,會傳遞到父視圖繼續調用setNeedsLayout,父視圖的setNeedsLayout可能又會調用到其他視圖的layout信息。如果我們能在這相互之後調用找到調用者,也就是那個view調用了這個方法,那我們就可以分析清楚這些setNeedsLayout從哪裡來,到哪裡去,就能找到死循環的地方了。
這些信息確實很難收集,這也是為何蘋果要為我們專門開發這樣一個工具,方便我們來調試,查找問題的原因。
開啟這個工具的開關在“Arguments”選項裡面。如下圖。
-UIViewLayoutFeedbackLoopDebuggingThreshold 100 // 50...1000 -NSViewLayoutFeedbackLoopDebuggingThreshold 100 // 50...1000 // Logs to com.apple.UIKit:LayoutLoop or com.apple.AppKit:LayoutLoop
UIView是在iOS裡面使用的,NSView是在macOS裡面使用的。一旦我們開啟了這個開關,那麼layout feedback loop debugger就會開始記錄每一個調用了setNeedsLayout的信息。
這裡我給它設置了閥值是100。
如果發現在一個Runloop中,layout在一個view上面調用的次數超過了閥值,這裡設置的是100,也就是說次數超過100,這個死循環還會在跑一小段,因為這個時候要給debugger一個記錄信息的時間。當記錄完成之後,就會立即拋出異常。並且信息會顯示在logs中。log會被記錄在com.apple.UIKit:LayoutLoop(iOS)/com.apple.AppKit:LayoutLoop(macOS)中
我們也可以打全局的異常斷點exception break point。
在調試窗口也可以用LLDB命令po出一些調試信息。
接下來看2個實用的例子。
1.Upstream Geometry Change
這裡有這麼多個view,層級如上圖。
現在右子串上面10個子view在一次的層級變化中,被移除了。
那麼最上層圈起來的3個view都會被影響。於是這3個view的bounds就發生了變化。於是就會隱式的調用setNeedsLayout,來獲取新的bounds的信息。(這裡經過@kuailejim @冬瓜爭做全棧瓜 和大神們實驗,setNeedsLayout是需要我們開發者手動調用的,系統並不會在bounds改變的時候隱式調用setNeedsLayout方法)。當前view的bounds改變,但是如果父view沒有layout完成,那麼父view也會繼續收到setNeedsLayout消息。這個消息就會一直被往上傳遞,直到傳到最頂層的view,頂層的view layout完成之後,將會重置下面關聯的view的bounds,調用layoutSubview()方法。這時候,死循環就產生了。
這3個view就是上面3個view,下面的view需要setNeedsLayout,需要獲取最新的bounds信息,中間藍色的view也同樣需要setNeedsLayout,於是又會讓上層的view調用setNeedsLayout()方法,這個時候死循環就產生了。上下各有2個環,共同的view就是中間藍色的view。環內的view都在相互的請求setNeedsLayout(),並且在自己layout完成以後又會去重置關聯的view的bounds。這就形成了triggers layout。
大家對這裡產生2個環產生了極大的好奇,熱烈討論這裡會產生環的情況。目前可以想到會產生環的場景是這樣子的:在上面的3顆子樹,當某種場景下,突然刪掉了右邊的子樹,假設用戶的屏幕現在是全屏,由於一下子突然刪掉了一堆view,那麼原來那裡就會變成空白,這個時候開發者想要把其他的view平鋪到屏幕上。這個時候就需要改變上面父view的bounds,最下面的view會代碼裡面手動調用上面藍色的view,setNeedsLayout()方法,並且把藍色view的bounds設置成全屏,由於藍色view的bounds改變,這個時候開發者代碼裡面又手動調用了藍色view的父view,去執行setNeedsLayout()方法。top view代碼裡面又寫了bounds = origRect,這時候就觸發了藍色view的layout,更新bounds。這樣就產生了循環。同理下面也會形成循環。這樣就產生了2個死循環了。這些總結需要感謝@kuailejim @冬瓜爭做全棧瓜 給出的指點。
這裡是我們用工具收集到的log,第一行就是top-level view,接下來的就是遞歸的過程。往下看,我們會看見一些數字,這些數字就是view接到layout的次數,並且這些數字是有序的。一次死循環中這些數字就是循環時候的順序。當然一個循環中,每個view可以是起點也可以是終點。這裡我們默認把top view設置成起點。這樣就可以向我們展示出死循環中一共牽扯進來了多少個view。
從log上看,上面有3個view,下面有10個view,加起來也不等於23,這是為什麼呢?我們繼續往下看log,來看看“Views receiving layout in order”這裡面記錄了些什麼吧。
這裡我們可以很明顯的看到,view接收到layout的順序,一共正好23個。也可以看出,在一起循環中,一個view接收到layout的次數不止一次。
如上圖所標示的,有2段在循環,有10個view接收到layout之後,再是2個view,緊接著又是10個view,再是1個view。
回到最初我們使用這個工具的用途上來,最初我們使用這個工具是用來查看 top-level view 接收到setNeedsLayout消息到底從哪裡來。繼續往下查找,找到調用的棧信息那裡。
從上往下看,前幾行肯定都是UIViewLayoutFeedbackLoopDebugging的信息。往下看,看到第6行,可以看到DropShadowView接受到了信息,准備setBounds。回看之前的層級信息,我們會發現DropShadowView是TransitionView的子view。
引起DropShadowView觸發setBounds的唯一途徑是,它的父view,TransitionView觸發了setNeedsLayout()方法。因為這個時候TransitionView還沒有layout。
回到“geometry change records”,這個時候我們可以看到選中的這3行信息在一遍遍的循環。看第2行和第3行,我們可以看到是來自於TransitionView的layout。這時是合理的。再看第一行,會發現這個時候有一個TransitionView的子view調用了viewLayoutSubviews。
這個時候我們就定位到了bug的根源了,只要想方設法在layout的時候,不要改變superview的bounds即可以去掉這個死循環。
2.Ambiguous Layout From Constraints
在我們設置constraints約束的時候,常常會產生一些歧義的constraints。歧義的constraints通常不可怕,我們只需要稍稍做些調整,然後update all frame即可。
但是有如下的場景會導出形成環:
當你的view在旋轉之後,constraints也隨之變化,然後有些view在旋轉之後的constraint就會相互沖突。因次有些constraint就形成了環。
這個問題在沒有這個debugger工具的時候,思考起來很燒腦,沒有任何頭緒,這也是為什麼log把top-level view放在第一行的原因,給我們暗示,從這裡開始找bug的原因。
在log,我們會看到好多的“Ambiguous Layout”。注意:tAMIC是Translates Auto Resizing Mask into Constraints的縮寫。
我們來看看詳細的log。看log之前,我們應該知道,constraint雖然沖突很多,但是可能引起沖突的constraint只有一個,也就是說當我們更正了其中一個constraint,很可能所有的沖突都解決了。
如上圖log所示,在minX這裡我們設置了2個帶有沖突性的constraint,一個是-60,一個是-120。我們可以一個個的檢查約束,但是這個列表很長,檢查起來也比較麻煩。
那我們畫圖來分析一下這個問題。
如圖,label有leading和trailing padding,label是container的子view,container是action的子父,action是representation的子view。container和action view之間有一個居中的centering constraint。action view在representation view上有一個autoresizing mask constraints。
然後每個representation view之間是alignment對齊的。自此看來,這些view並沒有足夠的constraints能讓這些view都能確定位置信息。比如在X軸上,這一串view是可以存在在任何的位置,所以產生了歧義的constraint。
解決上面的歧義的
-UIViewLayoutFeedbackLoopDebuggingThreshold 100 // 50...1000 -NSViewLayoutFeedbackLoopDebuggingThreshold 100 // 50...1000 //Logs to com.apple.UIKit:LayoutLoop or com.apple.AppKit:LayoutLoop
用debugger就可以解決上述的問題。
總結
這個Xcode 8 給我們的Autolayout融合了之前Autoresizing masks的用法,使兩個合並在一起使用,這樣不同場景我們可以有更多的選擇,可以更加靈活的處理布局的問題。還允許我們能手動調節constraints警告優先級別。
針對macOS的布局問題,又給我們帶來了新的控件NSGridView
最後給我們帶來的新的調試Layout Feedback Loop Debugging的工具,能讓我們平時調試起來比較頭疼的問題,有了工具可以有據可循,迅速定位問題,查找問題。
最後,請大家多多指教。