深入剖析Auto Layout,分析iOS各版本新增特性

先前寫到的一篇Masonry心得文章里已經(jīng)提到了很多AutoLayout相關(guān)的知識(shí),這篇我會(huì)更加詳細(xì)的對其知識(shí)要點(diǎn)進(jìn)行分析和整理。

來歷

一般大家都會(huì)認(rèn)為Auto Layout這個(gè)東西是蘋果自己搞出來的,其實(shí)不然,早在1997年Alan Borning, Kim Marriott, Peter Stuckey等人就發(fā)布了《Solving Linear Arithmetic Constraints for User Interface Applications》論文(論文地址:http://constraints.cs.washington.edu/solvers/uist97.html)提出了在解決布局問題的Cassowary constraint-solving算法實(shí)現(xiàn),并且將代碼發(fā)布在他們搭建的Cassowary網(wǎng)站上http://constraints.cs.washington.edu/cassowary/。后來更多開發(fā)者用各種語言來寫Cassowary,比如說pybee用python寫的https://github.com/pybee/cassowary。自從它發(fā)布以來JavaScript,.NET,JAVA,Smalltall和C++都有相應(yīng)的庫。2011年蘋果將這個(gè)算法運(yùn)用到了自家的布局引擎中,美其名曰Auto Layout。

Cassowary

Cassowary是個(gè)解析工具包,能夠有效解析線性等式系統(tǒng)和線性不等式系統(tǒng),用戶的界面中總是會(huì)出現(xiàn)不等關(guān)系和相等關(guān)系,Cassowary開發(fā)了一種規(guī)則系統(tǒng)可以通過約束來描述視圖間關(guān)系。約束就是規(guī)則,能夠表示出一個(gè)視圖相對于另一個(gè)視圖的位置。

Auto Layout的生命周期

進(jìn)入下面主題前可以先介紹下加入Auto Layout的生命周期。在得到自己的layout之前Layout Engine會(huì)將Views,約束,Priorities(優(yōu)先級),instrinsicContentSize(主要是UILabel,UIImageView等)通過計(jì)算轉(zhuǎn)換成最終的效果。在Layout Engine里會(huì)有約束變化到Deferred Layout Pass再到應(yīng)用Run Loop再回到約束變化這樣的循環(huán)機(jī)制。

約束變化

觸發(fā)約束變化包括

  • Activating或Deactivating
  • 設(shè)置constant或priority
  • 添加和刪除視圖

這個(gè)Engine遇到約束變化會(huì)重新計(jì)算layout,獲取新值后會(huì)call它的superview.setNeedsLayout()

Deferred Layout Pass

在這個(gè)時(shí)候主要是做些容錯(cuò)處理,更新約束有些沒有確定或者缺失布局聲明的視圖會(huì)在這里處理。接著從上而下調(diào)用layoutSubviews()來確定視圖各個(gè)子視圖的位置,這個(gè)過程實(shí)際上就是將subview的frame從layout engine里拷貝出來。這里要注意重寫layoutSubviews()或者執(zhí)行類似layoutIfNeeded這樣可能會(huì)立刻喚起layoutSubviews()的方法,如果要這樣做需要注意手動(dòng)處理的這個(gè)地方自己的子視圖布局的樹狀關(guān)系是否合理。

生命周期中需要注意的事項(xiàng)

  • 不要期望frame會(huì)立刻變化。
  • 在重寫layoutSubviews()時(shí)需要非常小心。

約束

Auto Layout你的視圖層級里所有視圖通過放置在它們里面的約束來動(dòng)態(tài)計(jì)算的它們的大小和位置。一般控件需要四個(gè)約束決定位置大小,如果定義了intrinsicContentSize的比如UILabel只需要兩個(gè)約束即可。

約束方程式

view1.attribute1 = mutiplier * view2.attribute2 + constant

redButton.left = 1.0 * yellowLabel.right + 10.0 //紅色按鈕的左側(cè)距離黃色label有10個(gè)point

使用API添加約束

使用NSLayoutConstraint類(最低支持iOS6)添加約束。NSLayoutConstraint官方參考:https://developer.apple.com/library/prerelease/ios/documentation/AppKit/Reference/NSLayoutConstraint_Class/index.html

[NSLayoutContraint constraintWithItem:view1
                                                 attribute:NSLayoutAttributeBottom
                                               relatedBy:NSLayoutRelationEqual
                                                    toItem:view2
                                                 attribute:NSLayoutAttributeBottom
                                                multiplier:1.0
                                                 constant:-5]

把約束用約束中兩個(gè)view的共同父視圖或者兩視圖中層次高視圖的- (void)addConstraint:(NSLayoutConstraint *)constraint方法將約束添加進(jìn)去。

使用VFL語言添加約束

先舉個(gè)簡單的例子并排兩個(gè)view添加約束

[NSLayoutConstraint constraintWithVisualFormat:@“[view1]-[view2]"
                                                                  options:0
                                                                  metrics:nil
                                                                     views:viewsDictionary;

viewDictionary可以通過NSDictionaryOfVariableBindings方法得到

UIView *view1 = [[UIView alloc] init];
UIView *view2 = [[UIView alloc] init];
viewsDictionary = NSDictionaryOfVariableBindings(view1,view2);

options

可以給這個(gè)位掩碼傳入NSLayoutFormatAlignAllTop使它們頂部對齊,這個(gè)值的默認(rèn)值是NSLayoutFormatDirectionLeadingToTrailing從左到右??梢允褂肗SLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom 表示兩個(gè)視圖的頂部和底部約束相同。

metrics

這個(gè)參數(shù)作用是替換VFL語句中對應(yīng)的值

CGRect viewFrame = CGRectMake(50, 50, 100, 100);
NSDictionary *views = NSDictionaryOfVariableBindings(view1, view2);
NSDictionary *metrics = @{@"left": @(CGRectGetMinX(viewFrame)),
                                           @"top": @(CGRectGetMinY(viewFrame)),
                                        @"width": @(CGRectGetWidth(viewFrame)),
                                       @"height": @(CGRectGetHeight(viewFrame))};
[view1 addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];

使用NSDictionaryOfVariableBindings(...)快速創(chuàng)建

NSNumber *left = @50;
NSNumber *top = @50;
NSNumber *width = @100;
NSNumber *height = @100;

NSDictionary *views = NSDictionaryOfVariableBindings(view1, view2);
NSDictionary *metrics = NSDictionaryOfVariableBindings(left, top, width, height);

[view1 addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]];

VFL幾個(gè)基本例子

  • [view1(50)]-10-[view2(100)] 表示view1寬50,view2寬100,間隔10
  • [view1(>=50@750)] 表示view1寬度大于50,約束條件優(yōu)先級為750(優(yōu)先級越大優(yōu)先執(zhí)行該約束,最大1000)
  • V:[view1][view2(==view1)] 表示按照豎直排,上面是view1下面是一個(gè)和它一樣大的view2
  • H:|-[view1]-[view2]-[view3(>=20)]-| 表示按照水平排列,|表示父視圖,各個(gè)視圖之間按照默認(rèn)寬度來排列

VFL介紹

無論使用哪種方法創(chuàng)建約束都是NSLayoutConstraint類的成員,每個(gè)約束都會(huì)在一個(gè)Objective-C對象中存儲(chǔ)y = mx + b規(guī)則,然后通過Auto Layout引擎來表達(dá)該規(guī)則,VFL也不例外。VFL由一個(gè)描述布局的文字字符串組成,文本會(huì)指出間隔,不等量和優(yōu)先級。官方對其的介紹:Visual Format Language https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/VisualFormatLanguage.html

VFL的語法

  • 標(biāo)準(zhǔn)間隔:[button]-[textField]
  • 寬約束:[button(>=50)]
  • 與父視圖的關(guān)系:|-50-[purpleBox]-50-|
  • 垂直布局:V:[topField]-10-[bottomField]
  • Flush Views:[maroonView][buleView]
  • 權(quán)重:[button(100@20)]
  • 等寬:[button(==button2)]
  • Multiple Predicates:[flexibleButton(>=70,<=100)]

注意事項(xiàng)

創(chuàng)建這種字符串時(shí)需要注意一下幾點(diǎn):

  • H:和V:每次都使用一個(gè)。
  • 視圖變量名出現(xiàn)在方括號(hào)中,例如[view]。
  • 字符串中順序是按照從頂?shù)降?,從左到?/li>
  • 視圖間隔以數(shù)字常量出現(xiàn),例如-10-。
  • |表示父視圖

使用Auto Layout時(shí)需要注意的點(diǎn)

  • 注意禁用Autoresizing Masks。對于每個(gè)需要使用Auto Layout的視圖需要調(diào)用setTranslatesAutoresizingMaskIntoConstraints:NO
  • VFL語句里不能包含空格和>,<這樣的約束
  • 布局原理是由外向里布局,最先屏幕尺寸,再一層一層往里決定各個(gè)元素大小。
  • 刪除視圖時(shí)直接使用removeConstraint和removeConstraints時(shí)需要注意這樣刪除是沒法刪除視圖不支持的約束導(dǎo)致view中還包含著那個(gè)約束(使用第三方庫時(shí)需要特別注意下)。解決這個(gè)的辦法就是添加約束時(shí)用一個(gè)局部變量保存下,刪除時(shí)進(jìn)行比較刪掉和先前那個(gè),還有個(gè)辦法就是設(shè)置標(biāo)記,constraint.identifier = @“What you want to call”。

布局約束規(guī)則

表達(dá)布局約束的規(guī)則可以使用一些簡單的數(shù)學(xué)術(shù)語,如下表

類型 描述
屬性 視圖位置 NSLayoutAttributeLeft, NSLayoutAttributeRight, NSLayoutAttributeTop, NSLayoutAttributeBottom
屬性 視圖前面后面 NSLayoutAttributeLeading, NSLayoutAttributeTrailing
屬性 視圖的寬度和高度 NSLayoutAttributeWidth, NSLayoutAttributeHeight
屬性 視圖中心 NSLayoutAttributeCenterX, NSLayoutAttributeCenterY
屬性 視圖的基線,在視圖底部上方放置文字的地方 NSLayoutAttributeBaseline
屬性 占位符,在與另一個(gè)約束的關(guān)系中沒有用到某個(gè)屬性時(shí)可以使用占位符 NSLayoutAttributeNotAnAttribute
關(guān)系 允許將屬性通過等式和不等式相互關(guān)聯(lián) NSLayoutRelationLessThanOrEqual, NSLayoutRelationEqual, NSLayoutRelationGreaterThanOrEqual
數(shù)學(xué)運(yùn)算 每個(gè)約束的乘數(shù)和相加性常數(shù) CGFloat值

約束層級

約束引用兩視圖時(shí),這兩個(gè)視圖需要屬于同一個(gè)視圖層次結(jié)構(gòu),對于引用兩個(gè)視圖的約束只有兩個(gè)情況是允許的。第一種是一個(gè)視圖是另一個(gè)視圖的父視圖,第二個(gè)情況是兩個(gè)視圖在一個(gè)窗口下有一個(gè)非nil的共同父視圖。

優(yōu)先級

哪個(gè)約束優(yōu)先級高會(huì)先滿足其約束,系統(tǒng)內(nèi)置優(yōu)先級枚舉值UILayoutPriority

enum {
    UILayoutPriorityRequired = 1000, //默認(rèn)的優(yōu)先級,意味著默認(rèn)約束一旦沖突就會(huì)crash
    UILayoutPriorityDefaultHigh = 750,
    UILayoutPriorityDefaultLow = 250,
    UILayoutPriorityFittingSizeLevel = 50,
};
typedef float UILayoutPriority;

IntrinsicContentSize / Compression Resistance Priority / Hugging Priority

具有instrinsic content size的控件,比如UILabel,UIButton,選擇控件,進(jìn)度條和分段等等,可以自己計(jì)算自己的大小,比如label設(shè)置text和font后大小是可以計(jì)算得到的。這時(shí)可以通過設(shè)置Hugging priority讓這些控件不要大于某個(gè)設(shè)定的值,默認(rèn)優(yōu)先級為250。設(shè)置Content Compression Resistance就是讓控件不要小于某個(gè)設(shè)定的值,默認(rèn)優(yōu)先級為750。加這些值可以當(dāng)作是加了個(gè)額外的約束值來約束寬。

布局過程

updateConstraints -> layoutSubViews -> drawRect

viewDidLayoutSubviews,-layoutSubviews

使用Auto Layout的view會(huì)在viewDidLayoutSubviews或-layoutSubview調(diào)用super轉(zhuǎn)換成具有正確顯示的frame值。

View的改變會(huì)調(diào)用哪些方法

  • 改變frame.origin不會(huì)掉用layoutSubviews
  • 改變frame.size會(huì)使 superVIew的layoutSubviews調(diào)用
  • 改變bounds.origin和bounds.size都會(huì)調(diào)用superView和自己view的layoutSubviews方法

Auto Layout的Debug

Auto Layout以下幾種情況會(huì)出錯(cuò)

  • Unsatisfiable Layouts:約束沖突,同一時(shí)刻約束沒法同時(shí)滿足。系統(tǒng)發(fā)現(xiàn)時(shí)會(huì)先檢測那些沖突的約束,然后會(huì)一直拆掉沖突的約束再檢查布局直到找到合適的布局,最后日志會(huì)將沖突的約束和拆掉的約束打印在控制臺(tái)上。
  • Ambiguous Layouts:約束有缺失,比如說位置或者大小沒有全指定到。還有種情況就是兩個(gè)沖突的約束的權(quán)重是一樣的就會(huì)崩。
  • Logical Errors:布局中的邏輯錯(cuò)誤。
  • 不含視圖項(xiàng)的約束不合法,每個(gè)約束至少需要引用一個(gè)視圖,不然會(huì)崩。在刪除視圖時(shí)一定要注意。

Debugger

  • po [[UIWindow keyWindow] _autolayoutTrace]

參考

參考官方文檔:https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/TypesofErrors.html

容易出問題的Bug Case

  • 無共同父視圖的視圖之間相互添加約束會(huì)有問題。

  • 調(diào)用了setNeedsLayout后不能通過frame改變視圖和控件

  • 為了讓在設(shè)置了setTranslatesAutoresizingMaskIntoConstraints:NO視圖里更改的frame立刻生效而執(zhí)行了沒有標(biāo)記立刻刷新的layoutIfNeeded的方式是不可取的。

實(shí)踐中碰到的非必現(xiàn)低配置機(jī)器崩潰bug分析

案例一

一個(gè)視圖缺少高寬約束,在設(shè)置完了約束后執(zhí)行l(wèi)ayoutIfNeeded,然后設(shè)置寬高,這種情況在低配機(jī)器上可能會(huì)出現(xiàn)崩問題。原因在于layoutIfNeeded需要有標(biāo)記才會(huì)立刻調(diào)用layoutSubview得到寬高,不然是不會(huì)馬上調(diào)用的。頁面第一次顯示是會(huì)自動(dòng)標(biāo)記上需要刷新這個(gè)標(biāo)記的,所以第一次看顯示都是看不出問題的,但頁面再次調(diào)用layoutIfNeeded時(shí)是不會(huì)立刻執(zhí)行l(wèi)ayoutSubview的(但之前加上setNeedsLayout就會(huì)立刻執(zhí)行),這時(shí)改變的寬高值會(huì)在上文生命周期中提到的Auto Layout Cycle中的Engine里的Deferred Layout Pass里執(zhí)行l(wèi)ayoutSubview,手動(dòng)設(shè)置的layoutIfNeeded也會(huì)執(zhí)行一遍layoutSubview,但是這個(gè)如果發(fā)生在Deferred Layout Pass之后就會(huì)出現(xiàn)崩的問題,因?yàn)楫?dāng)視圖設(shè)置為setTranslatesAutoresizingMaskIntoConstraints:NO時(shí)會(huì)嚴(yán)格按照約束->Engine->顯示這種流程,如在Deferred Layout Pass之前設(shè)置好是沒有問題的,之后強(qiáng)制執(zhí)行LayoutSubview會(huì)產(chǎn)生一個(gè)權(quán)重和先前一樣的約束在類似動(dòng)畫block里更新布局讓Engine執(zhí)行導(dǎo)致Ambiguous Layouts這種權(quán)重相同沖突崩潰的情況發(fā)生。

案例二

將多個(gè)有相互約束關(guān)系視圖removeFromSuperView后更新布局在低配機(jī)器上出現(xiàn)崩的問題。這個(gè)原因主要是根據(jù)不含視圖項(xiàng)的約束不合法這個(gè)原則來的,同時(shí)會(huì)拋出野指針的錯(cuò)誤。在內(nèi)存吃緊機(jī)器上,當(dāng)應(yīng)用占內(nèi)存較多系統(tǒng)會(huì)抓住任何可以釋放heap區(qū)內(nèi)存的機(jī)會(huì)視圖被移除后會(huì)立刻被清空,這時(shí)約束如果還沒有被釋就滿足不含視圖項(xiàng)的約束會(huì)崩的情況了。

推薦Auto Layout第三方庫

Masonry

Github地址:https://github.com/SnapKit/Masonry

Cartography

Github地址:https://github.com/robb/Cartography

Masonry

可以參看我上篇文章《AutoLayout框架Masonry使用心得》:http://www.starming.com/index.php?v=index&view=81

各版本iOS中AutoLayout的區(qū)別

完整記錄可以到官方網(wǎng)站進(jìn)行核對和查找:What’s New in iOS https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Introduction/Introduction.html

iOS6

蘋果在這個(gè)版本引入Auto Layout,具備了所有核心功能。

iOS7

  • NavigationBar,TabBar和ToolBar的translucent屬性默認(rèn)為YES,當(dāng)前ViewController的高度是整個(gè)屏幕的高度,為了確保不被這些Bar覆蓋可以在布局中使用topLayoutGuide和bottomLayoutGuide屬性。
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[topLayoutGuide]-[view1]" options:0 metrics:nil views:view2];

iOS8

  • Self Sizing Cells http://www.appcoda.com/self-sizing-cells/
  • UIViewController新增兩個(gè)方法,用來處理UITraitEnvironment協(xié)議,UIKit里有UIScreen,UIViewController,UIView和UIPresentationController支持這個(gè)協(xié)議,當(dāng)視圖traitCollection改變時(shí)UIViewController時(shí)可以捕獲到這個(gè)消息進(jìn)行處理的。
- (void)setOverrideTraitCollection:(UITraitCollection *)collection forChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0);  
- (UITraitCollection *)overrideTraitCollectionForChildViewController:(UIViewController *)childViewController NS_AVAILABLE_IOS(8_0); 
  • Size Class的出現(xiàn)UIViewController提供了一組新協(xié)議來支持UIContentContainer
- (void)systemLayoutFittingSizeDidChangeForChildContentContainer:(id )container NS_AVAILABLE_IOS(8_0);  
- (CGSize)sizeForChildContentContainer:(id )container withParentContainerSize:(CGSize)parentSize NS_AVAILABLE_IOS(8_0);  
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator NS_AVAILABLE_IOS(8_0);  
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection withTransitionCoordinator:(id )coordinator NS_AVAILABLE_IOS(8_0);
  • UIView的Margin新增了3個(gè)API,NSLayoutMargins可以定義view之間的距離,這個(gè)只對Auto Layout有效,并且默認(rèn)值為{8,8,8,8}。NSLayoutAttribute的枚舉值也有相應(yīng)的更新
//UIView的3個(gè)Margin相關(guān)API
@property (nonatomic) UIEdgeInsets layoutMargins NS_AVAILABLE_IOS(8_0);  
@property (nonatomic) BOOL preservesSuperviewLayoutMargins NS_AVAILABLE_IOS(8_0);  
- (void)layoutMarginsDidChange NS_AVAILABLE_IOS(8_0); 
//NSLayoutAttribute的枚舉值更新
NSLayoutAttributeLeftMargin NS_ENUM_AVAILABLE_IOS(8_0),  
NSLayoutAttributeRightMargin NS_ENUM_AVAILABLE_IOS(8_0),  
NSLayoutAttributeTopMargin NS_ENUM_AVAILABLE_IOS(8_0),  
NSLayoutAttributeBottomMargin NS_ENUM_AVAILABLE_IOS(8_0),  
NSLayoutAttributeLeadingMargin NS_ENUM_AVAILABLE_IOS(8_0),  
NSLayoutAttributeTrailingMargin NS_ENUM_AVAILABLE_IOS(8_0),  
NSLayoutAttributeCenterXWithinMargins NS_ENUM_AVAILABLE_IOS(8_0),  
NSLayoutAttributeCenterYWithinMargins NS_ENUM_AVAILABLE_IOS(8_0), 

iOS9

UIStackView

蘋果一直希望能夠讓更多的人來用Auto Layout,除了弄出一個(gè)VFL現(xiàn)在又弄出一個(gè)不需要約束的方法,使用Stack view使大家使用Auto Layout時(shí)不用觸碰到約束,官方口號(hào)是“Start with Stack View, use constraints as needed”。 更多細(xì)節(jié)可以查看官方介紹:UIKit Framework Reference UIStackView Class Referencehttps://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/AutoLayoutWithoutConstraints.html

Stack Views :https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/LayoutUsingStackViews.html

Stack View提供了更加簡便的自動(dòng)布局方法比如Alignment的Fill,Leading,Center,Trailing。Distribution的Fill,F(xiàn)ill Equally,F(xiàn)ill Proportionally,Equal Spacing。

如果希望在iOS9之前的系統(tǒng)也能夠使用Stack view可以用sunnyxx的FDStackViewhttps://github.com/forkingdog/FDStackView,利用運(yùn)行時(shí)替換元素的方法來支持iOS6+系統(tǒng)。

NSLayoutAnchorAPI

新增這個(gè)API能夠讓約束的聲明更加清晰,還能夠通過靜態(tài)類型檢查確保約束的正常工作。具體可以查看官方文檔https://developer.apple.com/library/ios/documentation/AppKit/Reference/NSLayoutAnchor_ClassReference/

NSLayoutConstraint *constraint = [view1.leadingAnchor constraintEqualToAnchor:view2.topAnchor];

參考

官方文檔

WWDC視頻

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容