iOS 開發實踐之 Auto Layout
本文是博主 iOS 開發實踐系列中的一篇,主要講述 iOS 中 Auto Layout(自動布局)在實際項目中的使用。
Auto Layout 在 2012 年的 iOS 6 中發布,距今已經 2 年多了,如果從 2011 年在 Mac OS X 上發布的 Auto Layout 開始算起,已經超過 3 年了。如果你的簡歷上寫著 2 年以上工作經驗,而竟然不會使用 Auto Layout,真有點不可思議。
本文將會通過若干個 Demo 進行講解,通過實踐來理解 Auto Layout 到底是什么,該如何使用(包括在 Xib 中使用以及手動編碼)。
Auto Layout 是什么?
我的理解:Auto Layout 是一種基于約束的布局系統,它可以根據你在元素(對象)上設置的約束自動調整元素(對象)的位置和大小。
官方的說明:
Auto Layout 是一個系統,可以讓你通過創建元素之間關系的數學描述來布局應用程序的用戶界面。——《Auto Layout Guide》
Auto Layout 是一種基于約束的,描述性的布局系統。——《Taking Control of Auto Layout in Xcode 5 - WWDC 2013》
</blockquote>這里有幾個關鍵字:
- 元素
- 關系
- 約束
- 描述
</ul>元素(Element)
低頭看看你電腦的鍵盤,你可以把每一個按鍵當做一個元素;對于 iOS 系統來說,你可以把桌面上每一個應用圖標當做一個元素;對于某一款 iOS 應用來說,你可以把視圖中的每一個子視圖當做一個元素。
事實上,你也可以把整個鍵盤、桌面或者視圖當做一個元素。
關系(Relation)
元素之間可以有關系。例如在鍵盤上Q鍵和W鍵之間有關系。是什么關系呢?有很多,例如Q鍵在W鍵的左邊,W鍵在Q鍵的右邊,Q鍵和W鍵之間相距 0.5 厘米等等。
不理解?試著把鍵盤想象成View,把按鍵想象成Button,再思考一遍。
約束(Constraint)
元素之間關系的限制。約束是 Auto Layout 系統中最重要的概念。我們上面提到的左邊、右邊以及相距 0.5 厘米等這些都是約束,它們限制了元素之間的關系。
描述(Description)
定義約束來限制元素之間的關系。描述定義了元素之間的關系及約束。
繼續用鍵盤舉例,Q鍵的長寬均為 1 厘米,左邊距離鍵盤的左邊緣 10 厘米,上邊距離鍵盤的頂部 5 厘米。這句話就可以定位Q鍵在鍵盤中的位置,很輕松就可以計算出Q鍵的frame為{{10.0, 5.0}, {1.0, 1.0}}。
現在Q鍵的坐標已經確定,那么W鍵的坐標可以這樣描述:頂部和Q鍵對齊,大小和Q鍵相等,位于Q鍵右側 0.5 厘米處。仔細想想,這句話中包含了元素間的關系,關系間的約束,可以直接計算出W鍵的frame。
忘掉傳統的 Springs & Struts 布局方式
事實上如果你用傳統的設置 frame 的布局方式的思維來理解上面的Q鍵和W鍵的布局也說的通。
因為在 Auto Layout 中,當你描述完之后, Auto Layout 會自動幫你計算出 frame。換句話說,你的描述告訴了 Auto Layout 如何幫你計算出 frame。所以,你也可以理解為你間接的設置了 frame。為什么要這么做呢?為什么不直接設置 frame?這是因為使用 Auto Layout 有很多好處:
- 多數情況下旋轉屏幕不用再做額外的處理
- 更容易適配不同尺寸的屏幕
- 上手后布局非常簡單容易,布局邏輯更清晰
</ul>Auto Layout 和傳統布局很大的不同之處在于它是一種相對的布局方式。怎么理解這句話?上面提到
W鍵位于Q鍵右側 0.5 厘米處。
</blockquote>傳統的布局無法直接表示,你必須把這種布局手動轉換為傳統布局代碼。例如上面的Q鍵和W鍵的傳統布局代碼看起來可能是這樣:
q.frame = CGRectMake(CGRectGetMinX(keyBoard.frame) + 10.f, CGRectGetMinY(keyBoard.frame) + 5.f, 1.f, 1.f); w.frame = CGRectMake(CGRectGetMaxX(q.frame) + 0.5f, CGRectGetMinY(q.frame), CGRectGetWidth(q.frame), CGRectGetHeight(q.frame));使用 Auto Layout 的布局代碼看起來像這樣:
// 偽代碼 q.width = 1.f; q.height = 1.f; q.left = keyboard.left + 10.f; q.top = keyboard.top + 5.f;w.top = q.top; w.width = q.width; w.height = q.height; w.left = q.right + .5f;</pre>
Auto Layout 不僅能輕松表示這種布局,而且相對于傳統的布局更清晰簡潔易懂,還免費附贈很多優點,有什么理由不使用 Auto Layout 呢?
實踐中我發現對于很多新手來說,Auto Layout 這種布局方式比較容易理解接受,相反很多對傳統布局很熟練的人卻不太容易理解,總是用傳統布局的思維來思考,所以如果可能的話,我建議你暫時忘掉傳統的布局方式。
Autoresizing Mask
事實上我不打算講這個東西,以及它和 Auto Layout 的區別和聯系。如果你不知道,對學習 Auto Layout 不會有什么影響。
你唯一需要注意的是在使用 Auto Layout 時,首先需要將視圖的translatesAutoresizingMaskIntoConstraints屬性設置為NO。這個屬性默認為YES,如果你是使用 Xib 的話,這個屬性會自動幫你設置為NO。當它為YES時,運行時系統會自動將 Autoresizing Mask 轉換為 Auto Layout 的約束,這些約束很有可能會和我們自己添加的產生沖突。
Auto Layout 基礎知識
無論是在 Xib 中還是代碼中使用 Auto Layout,你都需要了解 Auto Layout 的一些必要知識。這些你現在不理解沒有關系,后面我們會詳細講述。
約束 (Constraint)
Auto Layout 中約束對應的類為NSLayoutConstraint,一個NSLayoutConstraint實例代表一條約束。
NSLayoutConstraint有兩個方法,第一個是
+ (id)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attribute1 relatedBy:(NSLayoutRelation)relation toItem:(id)view2 attribute:(NSLayoutAttribute)attribute2 multiplier:(CGFloat)multiplier constant:(CGFloat)constant;不要被這個方法的參數嚇到,實際上它只做一件事,就是讓view1的某個attribute等于view2的某個attribute的multiplier倍加上constant,
這里的attribute可以是上下左右寬高等等。
精簡后就是下面這個公式:
view1.attribute1 = view2.attribute2 × multiplier + constant還有一個參數是relation,這是一個關系參數,它標明了上面這個公式兩邊的關系,它可以是小于等于 (≤),等于 (=)和大于等于 (≥)。上面的公式假定了這個參數傳入的是=,根據參數的不同,公式中的關系符號也不同。
需要注意的是,≤或≥優先會使用=關系,如果=不能滿足,才會使用<或>。例如設置一個≥ 100的關系,默認會是 100,當視圖被拉伸時,100 無法被滿足,尺寸才會變得更大。
例子:
1、我們要實現一個如下圖的布局。
![]()
布局代碼如下:
UIView *view = [UIView new]; [view setBackgroundColor:[UIColor redColor]]; [self.view addSubview:view];CGRect viewFrame = CGRectMake(50.f, 100.f, 150.f, 150.f);
// 使用 Auto Layout 布局 [view setTranslatesAutoresizingMaskIntoConstraints:NO];
//
view
的左邊距離self.view
的左邊 50 點. NSLayoutConstraint viewLeft = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1 constant:CGRectGetMinX(viewFrame)]; //view
的頂部距離self.view
的頂部 100 點. NSLayoutConstraint viewTop = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:CGRectGetMinY(viewFrame)]; //view
的寬度 是 60 點. NSLayoutConstraint viewWidth = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:CGRectGetWidth(viewFrame)]; //view
的高度是 60 點. NSLayoutConstraint viewHeight = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:CGRectGetHeight(viewFrame)]; // 把約束添加到父視圖上. [self.view addConstraints:@[viewLeft, viewTop, viewWidth, viewHeight]];</pre>實現一個如此簡單的布局竟然要寫這么多的代碼,這顯然難于推廣使用。于是 UIKit 團隊發明了另外一種更簡便的表達方式進行布局,這個我們后面再講,現在先看看這段代碼。
首先我把view的translatesAutoresizingMaskIntoConstraints設為了NO,禁止將 Autoresizing Mask 轉換為約束。
然后在設置viewLeft這個約束時,attribute參數使用了NSLayoutAttributeLeading而不是NSLayoutAttributeLeft,這兩個參數值都表示左邊,但它們之間的區別在于NSLayoutAttributeLeft永遠表示左邊,但NSLayoutAttributeLeading是根據習慣區分的,例如在某些文字從右向左閱讀的地區,例如阿拉伯,NSLayoutAttributeLeading表示右邊。換句話說,NSLayoutAttributeLeading是表示文字開始的方向。在英文、中文這種從左往右閱讀的文字中它表示左邊,在像阿拉伯語、希伯來語這種從右往左閱讀的文字中它表示右邊。通常情況下,除非你明確要限制在左邊,否則你都應該使用NSLayoutAttributeLeading表示左邊。相對的,表示右邊也類似這樣。這對于我們的本地化工作有很大的幫助。
然后在設置viewWidth和viewHeight這兩個約束時,relatedBy參數使用的是NSLayoutRelationGreaterThanOrEqual而不是NSLayoutRelationEqual。
因為 Auto Layout 是相對布局,所以通常你不應該直接設置寬度和高度這種固定不變的值,除非你很確定視圖的寬度或高度需要保持不變。
如果一定要設置高度或寬度,特別是寬度,在沒有顯式地設置內容壓縮優先級(Content Hugging Priority,后面會講到)和內容抗壓縮優先級(Content Compression Resistance Priority,后面會講到)的情況下,盡量不要使用NSLayoutRelationEqual這種絕對的關系,這會帶來許多潛在的問題:
- 根據內容決定寬度的視圖,當內容改變時,外觀尺寸無法做出正確的改變
- 在本地化時過長的文字無法顯示,造成文字切斷,或文字過短,寬度顯得過寬,影響美觀
- 添加了多余的約束時,約束之間沖突,無法顯示正確的布局
</ul>所帶來的問題不僅僅局限與這幾條,這里只是簡單列出幾條。
如何正確的設置寬度或高度?給出一些 Tips:
- 如果寬度和高度布局可以改變,使用固有內容尺寸(Intrinsic Content Size,后面會講到)設置約束(即 size to fit size)。
- 如果寬度和高度布局不可以改變,改變約束的關系為≥。
- 調整壓縮優先級和內容抗壓縮優先級
</ul>最后我把所有約束都添加到了view的父視圖self.view上。view的約束為什么不添加到自身而添加到別的視圖上去呢?這是由于約束是根據視圖層級自下而上更新的,也就是從子視圖到父視圖。所以 Auto Layout 添加約束有一套自己的規則,如下:
- 兩個同層級間視圖的約束,添加到它們共同的父視圖上
</ul>
![]()
- 兩個不同層級間視圖的約束,添加到它們最近的共同的父視圖上
</ul>
![]()
- 兩個有層級關系的視圖的約束,添加到層次較高的視圖上(父視圖)上
</ul>
![]()
因為我們屬于最后一種情況,所以子視圖view的約束添加到了父視圖self.view上。
接下來是第二個方法
+ (NSArray *)constraintsWithVisualFormat:(NSString *)format options:(NSLayoutFormatOptions)opts metrics:(NSDictionary *)metrics views:(NSDictionary *)views;這個方法是我們實際編程中最常用的方法。它會根據我們指定的參數返回一組約束。這個方法很重要,所以我會詳細解釋每個參數的用途。
format
這個參數存放的是布局邏輯,布局邏輯是使用 可視化格式語言 (VFL) 編寫的。實際編程中我們也是使用VFL編寫布局邏輯,因為第一個方法明顯參數過多,一個簡單的布局要寫很多代碼。
上一個布局使用VFL來重構的話,代碼如下:
.... [view setTranslatesAutoresizingMaskIntoConstraints:NO]; NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view); [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];嘩,代碼量減少了很多。首先我們使用NSDictionaryOfVariableBindings(...)宏創建了一個字典views,這個宏會自動把傳入的對象的鍵路徑作為字典的鍵,把對象作為字典的值。所以views字典的內容就像這樣:
{@"self.view": self.view, @"view", view}VFL就是這兩句:
H:|-50-[view(>=150)]
V:|-100-[view(>=150)]
第一句是在水平方向布局,表示view左邊距離父視圖左邊 50 點,寬度至少 150 點。(水平方向是寬度)
第二句是在垂直方向上布局,表示view頂部距離父視圖頂部 100 點,寬度至少 150 點。(垂直方向是高度)
分解說明如下:
H/V表示布局方向。H表示水平方向(Horizontal),V表示垂直方向(Vertical),方向后要緊跟一個:,不能有空格。
|表示父視圖。通常出現在語句的首尾。
-有兩個用途,單獨一個表示標準距離。這個值通常是 8 ;兩個中間夾著數值,表示使用中間的數值代替標準距離,如第一句的-50-,就是使用 50 來代替標準距離。
[]表示對象,括號中間需要填上對象名,對象名必須是我們傳入的views字典中的鍵。對象名后可以跟小括號(),小括號中是對此對象的尺寸和優先級約束。水平布局中尺寸是寬度,垂直布局中尺寸是高度。如第一句中的(>=150)就是對view尺寸的約束,因為是水平方向布局,所以它表示寬度大于或等于 150 點。而 150 前面的>=就是我們上面第一個方法中提到的關系參數。至于為什么這里使用>=,上面已經解釋過了。括號中可以包含多條約束,如果我們想再加一條約束,保證view的寬度最大不超過 200 點,我們可以這樣寫:H:|-50-[view(>=150,<=200)]。還可以添加優先級約束,這個我們后面再講。
VFL語法有幾點需要注意:
- 布局語句中不能包含空格
- 和關系一樣,沒有>、<這種約束
</ul>然后下面是一些例子,增加你對VFL語法的理解。
例一:
我們在view右側添加另一個視圖view2,效果如圖:
![]()
代碼如下:
UIView *view = [UIView new]; [view setBackgroundColor:[UIColor redColor]]; [self.view addSubview:view];UIView *view2 = [UIView new]; [view2 setBackgroundColor:[UIColor blueColor]]; [self.view addSubview:view2];
[view setTranslatesAutoresizingMaskIntoConstraints:NO]; [view2 setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2); [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[view]-[view2(>=50)]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view2(>=50)]" options:0 metrics:nil views:views]];</pre>
我們講講最后的兩條新的VFL語句:
H:[view]-[view2(>=50)]
從開始的H:我們可以判斷出這是水平方向的布局,換句話說就是設置視圖的x和width。接著的[view],說明后面的所有視圖都是在view的右側;接著是-,說明后一個視圖和view之間有一個標準距離的間距;也就是說 x 等于view的右側再加上標準距離,即CGRectGetMaxX(view) + 標準距離。最后是[view2(>=50)],這里可以看出后一個視圖是view2,并且它的寬度不小于 50 點。整一句翻譯成白話就是說:在水平方向上,view2在view右側的標準距離位置處,并且它的寬度不小于 50 點。
V:|-100-[view2(>=50)]
從開始的V:我們可以判斷出這是垂直方向的布局,換句話說就是設置視圖的y和height。接著的|說明是后一個視圖是相對于父視圖進行布局;接著是-100-,說明垂直方向和父視圖(頂部)相距 100 點,也就是說 y 等于 100 點。最后是[view2(>=50)],這和上一句相同,只是因為是垂直方向,所以 50 是設置高度而不是寬度。整一句翻譯成白話就是說:在垂直方向上,view2在相對于父視圖(頂部) 100 點的位置處,并且它的高度不小于 50 點。
實際上我們的代碼還可以簡化:
...... NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2); [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]-[view2(>=50)]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view2(>=50)]" options:0 metrics:nil views:views]];因為兩個視圖水平方向上是并排(從左到右)的,所以我們可以將水平方向布局的代碼合并到一起。而垂直方向我們并非并排的,所以垂直方向的布局代碼我們不能合并。這里所講的并排的意思是后一個在前一個的后面,水平方向上明顯是這樣,但垂直方向上兩個視圖的y是相同的,所以無法合并在一起布局。
例二:我們繼續添加一個視圖view3填補view右下方的空缺,效果如圖:
![]()
代碼如下:
UIView *view = [UIView new]; [view setBackgroundColor:[UIColor redColor]]; [self.view addSubview:view];UIView *view2 = [UIView new]; [view2 setBackgroundColor:[UIColor blueColor]]; [self.view addSubview:view2];
UIView *view3 = [UIView new]; [view3 setBackgroundColor:[UIColor orangeColor]]; [self.view addSubview:view3];
[view setTranslatesAutoresizingMaskIntoConstraints:NO]; [view2 setTranslatesAutoresizingMaskIntoConstraints:NO]; [view3 setTranslatesAutoresizingMaskIntoConstraints:NO];
NSDictionary views = NSDictionaryOfVariableBindings(self.view, view, view2, view3); [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(50)-[view(>=150)]-[view2(>=50)]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(100)-[view(>=150)]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:[view]-[view3(>=50)]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(100)-[view2(>=50)][view3(>=100)]" options:0 metrics:nil views:views]];</pre>
你可能注意到我把每個間距都使用小括號闊了起來,這是可選的,你完全可以直接寫間距,這么寫只是告訴你還有這種語法。實際上沒什么必要這么寫,因為VFL語法并不支持運算,例如把(50)切分為(10+40)或(5
10)都是不合法的。 </p>最后兩行是view3的布局代碼,簡單解釋一下:
H:[view]-[view3(>=50)]
水平方向布局,view3在view右側標準距離處,并且寬度不小于 50 點。
V:|-(100)-[view2(>=50)][view3(>=100)]
垂直方向布局,view2距離父視圖(頂部)100 點,并且高度不小于 50 點;view3緊挨著view2底部(沒有-),并且高度不小于 100 點。
options
這個參數的值是位掩碼,使用頻率并不高,但非常有用。它可以操作在VFL語句中的所有對象的某一個屬性或方向。例如上面的例一,水平方向有兩個視圖,它們的垂直方向到頂部的距離相同,或者說頂部對齊,我們就可以給這個參數傳入NSLayoutFormatAlignAllTop讓它們頂部對齊,這樣以來只需要指定兩個視圖的其中一個的垂直方向到頂部的距離就可以了。代碼:
...... NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]-[view2(>=50)]" options:NSLayoutFormatAlignAllTop metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[view2(>=50)]" options:0 metrics:nil views:views]];它的默認值是NSLayoutFormatDirectionLeadingToTrailing,根據當前用戶的語言環境進行設置,比如英文中就是從左到右,希伯來語中就是從右到左。
這個值符合我們常用的選項。NSLayoutFormatDirectionLeadingToTrailing的值是0 << 16,所以我們可以直接傳入0使用此值。
因為是位掩碼,所以我們可以使用|進行多選,例如例一,我們希望在現有約束的基礎上讓兩個視圖的高度相等,那代碼可以這樣寫:
...... NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view, view2); [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-50-[view(>=150)]-[view2(>=50)]" options:NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom metrics:nil views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[view(>=150)]" options:0 metrics:nil views:views]];指定兩個視圖的頂部和底部約束相同,然后只設置其中一個視圖的相關約束即可。
靈活使用此參數可以節省不少時間,但這個參數內容太多,如果你有興趣了解,可以看看我的另一篇博文:《Auto Layout 中的排列選項》
metrics
這是一個字典,字典的鍵必須是出現在VFL語句中的字符串,值必須是NSNumber類型,作用是將在VFL語句中出現的鍵替換為相應的值。例如本文中的第一個布局的例子,使用了這個參數后代碼就變成了這樣:
UIView *view = [UIView new]; [view setBackgroundColor:[UIColor redColor]]; [self.view addSubview:view];[view setTranslatesAutoresizingMaskIntoConstraints:NO];
CGRect viewFrame = CGRectMake(50.f, 100.f, 150.f, 150.f);
NSDictionary *views = NSDictionaryOfVariableBindings(self.view, view);
NSDictionary *metrics = @{@"left": @(CGRectGetMinX(viewFrame)), @"top": @(CGRectGetMinY(viewFrame)), @"width": @(CGRectGetWidth(viewFrame)), @"height": @(CGRectGetHeight(viewFrame))};
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view(>=height)]" options:0 metrics:metrics views:views]];</pre>
聰明的你看了這段代碼后肯定已經明白這個參數的用途了,雖然使用頻率不高,但依然很有用,特別是要動態計算約束值的時候非常有用。
實際上這個參數也可以使用NSDictionaryOfVariableBindings(...)宏來快速創建,代碼如下:
...... [view setTranslatesAutoresizingMaskIntoConstraints:NO];NSNumber left = @50.f; NSNumber top = @100.f; NSNumber width = @150.f; NSNumber height = @150.f;
NSDictionary views = NSDictionaryOfVariableBindings(self.view, view); NSDictionary metrics = NSDictionaryOfVariableBindings(left, top, width, height);
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-left-[view(>=width)]" options:0 metrics:metrics views:views]]; [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-top-[view(>=height)]" options:0 metrics:metrics views:views]];</pre>
views
又是一個字典,包含了VFL語句中用到的視圖。字典的鍵必須是出現在VFL語句中的視圖名稱,值必須視圖的實例。這個字典我們在講format時已經講過,也用過很多次,相比你早已明白是怎么回事了。
講了這么多,可能你也發現了,只要學會了VFL語法,就可以方便地使用 Auto Layout 了,其他的知識都屬于輔助選項,會的話,布局更輕松一些,不會也沒關系,實踐多了,自然就會了。
優先級 (Priority level)
約束條件有優先級,高優先級約束會比低優先級約束優先得到滿足,系統內置了 4 個優先級:
enum { UILayoutPriorityRequired = 1000, UILayoutPriorityDefaultHigh = 750, UILayoutPriorityDefaultLow = 250, UILayoutPriorityFittingSizeLevel = 50, }; typedef float UILayoutPriority;
- UILayoutPriorityRequired 這是默認值,這意味著這個約束條件必須被精確地滿足。
- UILayoutPriorityDefaultHigh
- UILayoutPriorityDefaultLow
- UILayoutPriorityFittingSizeLevel 這是內置的最低優先級。
</ul>相信你已經看到每個等級的數值了,優先級的取值在0 ~ 1000之間,取值越大,優先級越高,越會被優先滿足。
每個約束的默認優先級就是UILayoutPriorityRequired,這意味著你給出的所有約束都必須得到滿足,一旦約束間發生沖突,你的應用就會 Crash。這也是在使用 Auto Layout 時經常會犯的錯誤:沒有給約束設置適當的優先級。
舉個例子說明優先級設置不當的情況,給我們首次使用 Auto Layout 時的例子再添加一個約束:
......//
view
的高度是 60 點. NSLayoutConstraint viewHeight = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationGreaterThanOrEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:CGRectGetHeight(viewFrame)]; //view
緊貼著self.view
的左邊. NSLayoutConstraint marginLeft = [NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1 constant:0];// 把約束添加到父視圖上. [self.view addConstraints:@[viewLeft, viewTop, viewWidth, viewHeight, marginLeft]];</pre>
運行看看效果,程序 Crash 了!控制臺 Log 中有這么一段信息:
"<NSLayoutConstraint:0xXXXXXXX H:|-(50)-[UIView:0xXXXXXX] (Names: '|':UIView:0xXXXXXX )>", "<NSLayoutConstraint:0xXXXXXXX H:|-(0)-[UIView:0xXXXXXX] (Names: '|':UIView:0xXXXXXX )>"可以看到第一條是viewLeft這個約束,它限制了view的左邊距離父視圖的左邊 50 點。
第二條是新添加的marginLeft這個約束,它限制了view的左邊距離父視圖的左邊 0 點,也就是緊貼著父視圖的左邊。
很明顯這兩個約束是沖突的,當系統嘗試根據優先級進行布局時,發現它們的優先級也相同,無法滿足兩個沖突的約束,所以拋出了異常。
我們只需要給兩個約束設置不同的優先級即可解決。添加下面一行代碼:
[viewLeft setPriority:UILayoutPriorityDefaultHigh];因為默認所有約束的優先級都是UILayoutPriorityRequired,所以我們只需要將viewLeft的優先級設置得比默認的低即可。
效果:
![]()
需要注意的一點是,約束的優先級必須在它添加到視圖上之前設置,如果約束已經添加到視圖上后去嘗試改變它的優先級,將會得到一個異常。
提高效率
Auto Layout 雖然很好,但無論是直接使用NSLayoutConstraint還是使用VFL來編寫布局的代碼都比較麻煩。
好消息是有大量的開源庫幫助我們提高編寫布局代碼的效率。比較流行的有:
- Masonry
- PureLayout(前 UIView-AutoLayout)
- FLKAutoLayout
- KeepLayout
</ul>我最初使用UIView-AutoLayout,但因為它不支持 OSX,所以后來使用過一段時間的Masonry,當UIView-AutoLayout的原作者發布PureLayout后,我就轉向了PureLayout并使用至今。
在我看來,Masonry和PureLayout差別并不大,PureLayout 的語法更偏向Objective-C。
下面是一個 Instagram 頁面截圖,我們使用PureLayout來實現這個布局。
![]()
我把它分為頭像、昵稱、時間標識、時間、贊標識、贊的數量、贊按鈕、評論按鈕、更多按鈕以及中間的圖片視圖。
聲明以下屬性:
@property (nonatomic, strong) UIImageView *avatarImageView; @property (nonatomic, strong) UILabel *nicknameLabel; @property (nonatomic, strong) UIView *timestampIndicator; @property (nonatomic, strong) UILabel *timestampLabel; @property (nonatomic, strong) UIImageView *contentImageView; @property (nonatomic, strong) UIView *likeIndicator; @property (nonatomic, strong) UILabel *likesLabel; @property (nonatomic, strong) UIButton *likeButton; @property (nonatomic, strong) UIButton *commentButton; @property (nonatomic, strong) UIButton *moreButton;布局代碼如下:
// 頭像左邊距離父視圖左邊 10 點. [self.avatarImageView autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:10.f];// 頭像頂邊距離父視圖頂部 10 點. [self.avatarImageView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:10.f];
// 設置頭像尺寸 [self.avatarImageView autoSetDimensionsToSize:kAvatarSize];
// 昵稱的左邊位于頭像的右邊 10 點的地方. [self.nicknameLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:self.avatarImageView withOffset:10.f];
// 根據昵稱的固有內容尺寸設置它的尺寸 [self.nicknameLabel autoSetDimensionsToSize:[self.nicknameLabel intrinsicContentSize]];
// 時間標識的右邊位于時間視圖左邊 -10 點的地方, 從右往左、從下往上布局時數值都是負的。 [self.timestampIndicator autoPinEdge:ALEdgeTrailing toEdge:ALEdgeLeading ofView:self.timestampLabel withOffset:-10.f];
// 根據時間標識的固有內容尺寸設置它的尺寸 [self.timestampIndicator autoSetDimensionsToSize:CGSizeMake(10.f, 10.f)];
// 時間視圖的右邊距離父視圖的右邊 10 點. [self.timestampLabel autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:10.f];
// 根據時間視圖的固有內容尺寸設置它的尺寸 [self.timestampLabel autoSetDimensionsToSize:[self.timestampLabel intrinsicContentSize]];
// 頭像、昵稱、時間標識、時間視圖水平對齊。(意思就是說只需要設置其中一個的垂直約束(y)即可) [@[self.avatarImageView, self.nicknameLabel, self.timestampIndicator, self.timestampLabel] autoAlignViewsToAxis:ALAxisHorizontal];
// 內容圖片視圖頂部距離頭像的底部 10 點. [self.contentImageView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.avatarImageView withOffset:10.f];
// 內容圖片視圖左邊緊貼父視圖左邊 [self.contentImageView autoPinEdgeToSuperviewEdge:ALEdgeLeading];
// 內容圖片視圖的寬度等于父視圖的寬度 [self.contentImageView autoMatchDimension:ALDimensionWidth toDimension:ALDimensionWidth ofView:self];
// 內容圖片視圖的高度等于父視圖的寬度 [self.contentImageView autoMatchDimension:ALDimensionHeight toDimension:ALDimensionWidth ofView:self];
// 贊標識與頭像左對齊 [self.likeIndicator autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:self.avatarImageView];
// 贊標識的頂部距離內容圖片視圖底部 10 點. [self.likeIndicator autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.contentImageView withOffset:10.f];
// 設置贊標識的尺寸 [self.likeIndicator autoSetDimensionsToSize:CGSizeMake(10.f, 10.f)];
// 贊數量視圖與贊標識水平對齊 [self.likesLabel autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.likeIndicator];
// 贊數量視圖的左邊距離贊標識的右邊 10 點. [self.likesLabel autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:self.likeIndicator withOffset:10.f];
// 以下請自行腦補... [self.likesLabel autoSetDimensionsToSize:[self.likesLabel intrinsicContentSize]];
NSArray *buttons = @[self.likeButton, self.commentButton, self.moreButton]; [buttons autoMatchViewsDimension:ALDimensionHeight]; [buttons autoAlignViewsToEdge:ALEdgeBottom]; [self.likeButton autoPinEdge:ALEdgeLeading toEdge:ALEdgeLeading ofView:self.avatarImageView]; [self.likeButton autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:10.f]; [self.likeButton autoSetDimensionsToSize:CGSizeMake(50.f, 25.f)];
[self.commentButton autoPinEdge:ALEdgeLeading toEdge:ALEdgeTrailing ofView:self.likeButton withOffset:5.f]; [self.commentButton autoSetDimension:ALDimensionWidth toSize:65.f];
[self.moreButton autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:10.f]; [self.moreButton autoSetDimension:ALDimensionWidth toSize:40.f];</pre>
效果完成:
來自:http://xuexuefeng.com/autolayout/![]()