1.AutoLayout是什么?
在Auto Layout之前,不論是在IB里拖放,還是在代碼中寫,每個UIView都會有自己的frame屬性,來定義其在當前視圖中的位置和尺寸。
UIView *view = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 10, 10)];
[self.view addSubview:view];
使用AutoLayout的話,就變為了使用約束條件來定義view的位置和尺寸。我們用"這個view距離底部10像素,距離頂部10像素,距離左邊10像素,距離y右邊像素"或"label A右邊緣和button B左邊緣有20點的空白空間。"這樣來描述view。
這樣最大好處是可以解決了不同分辨率和屏幕尺寸下view的適配問題,另外也簡化了旋轉時view的位置的定義,原來在底部之上10像素居中的view,不論在旋轉屏幕或是更換設備,始終還在底部之上10像素居中的位置,不會發生變化。
使用AutoLayout另一個重要的好處就是本地化。比如各自語言中中的文本寬度不同適配起來是一件很麻煩的事。AutoLayout能根據label需要顯示的內容自動改變label的大小。
2.AutoLayout和Autoresizing Mask的區別
如果你以前一直是代碼寫UI的話,你肯定寫過UIViewAutoresizingFlexibleWidth之類的枚舉;如果你以前用IB比較多的話,一定注意到過每個view的size inspector中都有一個紅色線條的Autoresizing的指示器和相應的動畫縮放的示意圖,這就是Autoresizing Mask。autosizing mask決定了當一個視圖的父視圖大小改變時,其自身需要做出什么改變。
autosizing mask下,你需要為每個view指定各自的寬高,并添加和父視圖的約束條件:
但AutoLayout則看起來簡單明了多了,視圖的大小和位置再也不重要了,只有約束要緊。當然,當你拖一個新建的button或label到畫布上時,它會有一定的大小,并且你會將它拖到某一位置,但這是只一個用來告訴Interface Builder如何放置約束的設計工具。
3.開始AutoLayout
我會從一個我們常見的用戶注冊頁面的例子講起
我們希望它在橫屏模式下也可以較好地展示出來。
我們在storyBoard中拖拽控件,注意當你拖拽的時候,藍色虛線將會出現。我們應該用這些虛線來做向導。通過preview(點擊show the Assistant Editor,并且切換到preview即可開啟)可以看出沒有AutoLayout的時候控件擺放非常糟糕。
首先對于左上方的imageView,我們希望它不管屏幕大小如何,都保持同樣的大小,所以我們需要做的是將鼠標放在改view上,按住control鍵,并垂直拖拽。效果如下:
我們點擊Height,這樣就規定了它的高度固定不變。同樣的道理,我們點擊Weight,這次你需要水平拖拽。
現在我們觀察preview,你會發現imageview跑到左上角去了。并且imageView會出現橙色的線。為什么呢?因為你的AutoLayout是不完整的,你只規定了高度和寬度,你沒有規定它距離它的父view邊緣的距離是多少。將鼠標放在改view上,按住control鍵,并拖拽至最外面的view上,放開:會看到下面的選項:
Top Space to Superview:是指該兩個view之間保存固定高度
Center Horizontal In Container:是指該兩個view之間垂直居中
Equal Widths:保持相同寬度
Equal Heights:保持相同高度
Aspect Ratio:保存固定比例關系
很明顯我們需要點擊第一個選項和第二個選項。第二個選項和imageView自身的width相當于規定了imageView到父view的左右邊緣長度不變。第一個選項和imageView自身的Height相當于規定了imageView到父view的上下高度不變。因此,該imageView的約束條件就完整了。
對于下面的UITextField,我們希望它距離上面的imageView高度固定,并且左右邊緣的距離固定。
我們將UITextField拖拽至imageView,放手如下:
Vertical Spacing:是指兩個view之間的垂直距離固定
Left:是指兩個view左邊對齊
Center X:是指兩個view左邊對齊
Right:是指兩個viewX軸中心對齊
我們需要點擊第一個選項來固定自身和上方imageView之間的距離,然后我們需要固定自身和父view邊緣的距離,所以我們拖拽至父view左右邊緣,如下圖:
Leading Space to Superview:view至父view左邊緣長度(前置距離)固定
Trailing Space to Superview:view至父view右邊緣長度(尾隨距離)固定
我們對第二個UITextField以及下面的UIButton進行同樣的操作。
當你對下面的UIButton進行同樣的操作后你會發現仍然出現了表示警告的橙色線,為什么呢?如果你不知道為什么,你可以看到Document Outline那里有一個黃色箭頭,點擊它,你會來到下圖所示:
它說:你期望的view高度是30,但現在它確實52,你需要修復它
你可能懷疑為什么button沒有Width約束,自動布局是為何知道button有多寬(30)的?
事情是這樣的:button自己是知道自己有多寬。它根據自己的title加上一些padding就行了。如果你為button的title設置更大的字號,它會自動調整它的寬度。
這正是我們熟悉的intrinsic content size。并不是所有的控制器都有這個,但大部分是(UILabel是一個例子)。如果一個視圖可以計算自己理想的大小,那么你就不需要為它特別指定Width或Height約束了。但在我們的例子中,我希望這個button更高啊,那怎么辦?
點擊那個黃色的三角形你會看到:
Update Frame?不,我們不想它的高度變小。Update Constrains?如果你點擊這個選項的話,你會發現什么變化都沒有,因為在它需要你沒有在Height上設置一個約束條件,也就談不上更新。那我們點擊第三個選項試試。
可以發現警告消失了,那我們點擊這個選項之后,XCode為我們做了什么?
看這里我們可以發現,UIButton自身增加了一個高度不變的約束條件,所以警告消失了。
我們運行看看效果如何
豎屏:
橫屏:
嗯,豎屏看起來還不錯,橫屏看起來不是太好,UIButton看不見了。這很好理解因為我們固定了控件和父view頂部的距離,但橫屏下高度變小所以UIButton被擠到下面去了。那怎么辦?如果我們固定了控件和父view底部的距離,很有可能會造成image在橫屏模式下被擠到上面去,所以有沒有更好的解決辦法呢。
其實上面講到的這些約束條件也是對象,它們是NSLayoutConstraint對象,所以我們可以在程序運行是動態改變其中的約束條件,如下圖,我們將imageView和父view的垂直距離約束條件拖動到代碼中:
在ViewController.m中寫下以下代碼:
-(void)viewWillLayoutSubviews{
if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)){
self.imageViewToViewSpace.constant = 30;
}
else{
self.imageViewToViewSpace.constant = 82;
}
}
效果看上去還不錯:
4.稍微復雜點的AutoLayout:
我們再將上面那個例子變得稍微復雜一點,把下面的Button變為3個button,每個等寬。
我們首先為button1增加上左右3個距離寬度約束條件:
為button2增加與button1等y軸,與button3距離約束,將button1與button2的距離和button2和button3的距離都設置為8:
為button3增加與button2等y軸,與父view的Trailing space:
看起來好多警告?恩下面是重點,我們把button1,button2,button3,設置為同樣高度和同樣寬度,警告就消失了。
現在運行效果如下:
5.SizeClass
我們上面使用了NSLayoutConstraint的IBOutlet對象,所以我們可以在程序運行是動態改變其中的約束條件,不過有另外一種更優雅的方式來實現上面的效果:使用SizeClass。
隨著iPhone6/iPhone6 Plus的發布,現在蘋果生態圈中的設備尺寸也已經變得種類繁多了。想必蘋果也意識到這一點。都知道蘋果是以化繁為簡的設計哲學深入人心的,這次再一次證明了。SizeClass是對設備尺寸的一個抽象概念,現在任何設備的 長、寬 被簡潔地分為三種情況:普通 (Regular) 、緊密 (Compact)和任意(Any) ,這樣,根據長和寬不同的搭配就能產生 3*3=9 種不同尺寸。下圖展示個每種情況對應的設備。
我們可以在不同的屏幕尺寸下使用不同的SizeClass,在正常情況下:
點擊 wAny,hAny可以更改需要布局的尺寸,顯然橫屏的時候,高度處于壓縮的狀態,(height: compact),我們需要先對正常的布局之外,還要添加一種(wAny, hCompact)
然后我們在這個狀態下重新設置我們的布局方式,把上面的imageView的topSpace 修改為10:
你需要知道的是在這個狀態下的布局方式不會影響其它size下的布局方式,預覽效果如下:
你有沒有注意到imageView的圖片不同了呢,你是不是以為我使用了不同的image?其實是同一張圖片,只不過我們可以在Images.xcassets上對不同size下使用不同的圖片:
1.AutoLayout與UITableView
你見識到了AutoLayout的強大之處了吧,下面的例子讓我們把AutoLayout應用到UITableView中,嘗試來構建更復雜的應用。例如下面圖片,當UITableView中內容不同時,使用AutoLayout來動態調整UITableCell的高度。
我們新建一個UITableViewController的子類,在我們的storyBoard中添加一個TableViewController,并將它的自定義類設置為DynamicCellHeightViewController。
在我們的cell上添加如下imageView和Label兩個控件:
新建自定義的cell類CustomTableViewCell并將storyBoard中的cell關聯至該類。
并將UILabel的屬性Lines設為了0以表示顯示多行。將cell的Identifier設置為"cell"。
讓我們給這些view一點約束。在上一篇文章你已經知道了通過按住ctrl在兩個view之間拖拽增加約束的方式,此外,我們還有其他兩種方式:用Editor\Pin和Align菜單:
還有在Interface Builder窗口的底部有一行這樣的按鈕:
從左到右分別是:對齊(Align),固定(Pin),解決自動布局問題(Resolve Auto Layout Issues)和重定義尺寸(Resizing Behavior)。前三個按鈕魚Editor菜單中的對應項有一致的功能。Resizing Behavior按鈕允許你在重新設置view的尺寸的時候,改變已經添加的約束。
頂部的Spacing to nearest neighbor可以添加上下左右四個約束條件,點擊者4個T字架,它們就會變成實體的紅色:
如上圖所示,我們為imageView增加4個約束條件。同理我們為label增加4個約束條件。
好了,我們已經完成了AutoLayout的布局,下面我們需要實現UITableView的協議。
先聲明了一個NSArray變量來存放數據。
@interface DynamicCellHeightViewController ()
@property (nonatomic, strong) NSArray *tableData;
@end
@implementation DynamicCellHeightViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableData = @[@"1\n2\n3\n4\n5\n6", @"123456789012345678901234567890", @"1\n2", @"1\n2\n3", @"1"];
}
現在實現UITableViewDataSource的protocol:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.tableData.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
cell.label.text = self.tableData[indexPath.row];
return cell;
}
從self.tableData中的數據我們可以看到,每一個Cell顯示的數據高度是不一樣的,那么我們需要動態計算Cell的高度。由于是自動布局,所以我們需要用到一個systemLayoutSizeFittingSize:來計算UITableViewCell所占空間高度。
這里有一個需要注意的問題,UITableView是一次性計算完所有Cell的高度,如果有1W個Cell,那么heightForRowAtIndexPath就會觸發1W次,然后才顯示內容。不過在iOS7以后,提供了一個新方法可以避免這1W次調用,它就是estimatedHeightForRowAtIndexPath。要求返回一個Cell的估計值,實現了這個方法,那只有顯示的Cell才會觸發計算高度的protocol. 由于systemLayoutSizeFittingSize需要cell的一個實例才能計算,所以這兒用一個成員變量存一個Cell的實列,這樣就不需要每次計算Cell高度的時候去動態生成一個Cell實例,這樣即方便也高效也少用內存,可謂一舉三得。
我們聲明一個存計算Cell高度的實例變量:
@property (nonatomic, strong) UITableViewCell *prototypeCell;
然后在viewDidLoad中初始化它:
self.prototypeCell = [self.tableView dequeueReusableCellWithIdentifier:@"cell"];
計算Cell高度的實現:
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomTableViewCell *cell = (CustomTableViewCell *)self.prototypeCell;
cell.label.text = [self.tableData objectAtIndex:indexPath.row];
CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
NSLog(@"h=%f", size.height + 1);
return 1 + size.height;//加1是因為分隔線的高度
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 66;
}
運行效果如下:
恩,好像哪里不對,計算cell高度時應該考慮頭像的高度。
如下圖,我們為頭像和cell之間添加一個約束條件,并在右面板中將Constant設置為>=10,這樣cell的最小高度也是頭像高度位置加上10了。
運行效果如下:
如果不用systemLayoutSizeFittingSize,我們也可以手動計算cell的高度,只要計算cell中label的文字高度即可,下面是該方法:
#import "NSString+addition.h"
@implementation NSString (addition)
- (CGSize)calculateSize:(CGSize)size font:(UIFont *)font {
CGSize expectedLabelSize = CGSizeZero;
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7) {
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attributes = @{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle.copy};
expectedLabelSize = [self boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size;
}
else {
expectedLabelSize = [self sizeWithFont:font
constrainedToSize:size
lineBreakMode:NSLineBreakByWordWrapping];
}
return CGSizeMake(ceil(expectedLabelSize.width), ceil(expectedLabelSize.height));
}
@end
像label這種控件會根據label中text的內容自動調整其高度,那如果是UITextView呢,我們需要下面的方法來返回其大小。
CGSize textViewSize = [cell.label sizeThatFits:CGSizeMake(cell.t.frame.size.width, FLT_MAX)];
2.Self Sizing
在iOS8中引入了一個強大的新特性,Self Sizing.只要在viewDidLoad中加入以下這兩行代碼,然后加入上面的自動布局,我們就可以把計算高度的代碼刪掉了。
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.estimatedRowHeight = 60.0;
self.tableView.rowHeight = UITableViewAutomaticDimension;
}
我自己在項目中也嘗試用過Self Sizing這個特性,不過當tableView加載特別多內容時會有明顯的卡頓效果并且tableView有時還會上下跳動!所以我對它的使用保留謹慎態度。不過,在構建簡單的tableView時,這是一個非常好用的特性。
AutoLayout與ScrollView
ScrollView在AutoLayout上的表現稍微有點特殊,我們來講講。首先我們拖動一個
ScrollView到視圖中,并設置它的約束條件:x , y , width , height.
UIScrollView特殊在于:需要設置其ContentView!,所以你需要另外拖一個UIView上作為它的內容視圖。
并且設置ContentView對應于UIScrollView的Leading Space、Trailing Space、Top Space、Bottom Space以及其width、height.我設置Leading Space、Trailing Space、Top Space、Bottom Space都為 0。
在這個例子里,我們需要內容視圖在ScrollView中滑起來,而且只能垂直滑動而不能水平滑動,所以我們需要把ContentView的寬設置成和ScrollView一樣,但是高一定要大于ScrollView的高:
你可以在這里下載完整的代碼。如果你覺得對你有幫助,希望你不吝嗇你的star:)