先前寫到的一篇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]
參考
容易出問題的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 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];
參考
官方文檔
- Auto Layout Guide https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/AutolayoutPG/index.html
- UIScrollView And Autolayout https://developer.apple.com/library/ios/technotes/tn2154/_index.html
- IB中使用Auto Layout:Auto Layout Help https://developer.apple.com/library/ios/recipes/xcode_help-IB_auto_layout/_index.html
- What’s New in iOS https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Introduction/Introduction.html
WWDC視頻
- WWDC 2012: Introduction to Auto Layout for iOS and OS X https://developer.apple.com/videos/wwdc/2012/?id=202
- WWDC 2012: Introducing Collection Views https://developer.apple.com/videos/play/wwdc2012-205/
- WWDC 2012: Advanced Collection Views and Building Custom Layouts https://developer.apple.com/videos/play/wwdc2012-219
- WWDC 2012: Best Practices for Mastering Auto Layout https://developer.apple.com/videos/wwdc/2012/?id=228
- WWDC 2012: Auto Layout by Example https://developer.apple.com/videos/wwdc/2012/?id=232
- WWDC 2013: Interface Builder Core Concepts https://developer.apple.com/videos/play/wwdc2013-405
- WWDC 2013: Taking Control of Auto Layout in Xcode 5 https://developer.apple.com/videos/wwdc/2013/?id=406
- WWDC 2015: Mysteries of Auto Layout, Part 1 內(nèi)容包含了Auto Layout更高封裝stack view的介紹 https://developer.apple.com/videos/play/wwdc2015-218/
- WWDC 2015: Mysteries of Auto Layout, Part 2 包含Auto Layout生命周期和調(diào)試Auto Layout的一些方法介紹https://developer.apple.com/videos/play/wwdc2015-219/