AutoLayout

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指定各自的寬高,并添加和父視圖的約束條件:

AutoLayout1.png
AutoLayout2.png

但AutoLayout則看起來簡單明了多了,視圖的大小和位置再也不重要了,只有約束要緊。當然,當你拖一個新建的button或label到畫布上時,它會有一定的大小,并且你會將它拖到某一位置,但這是只一個用來告訴Interface Builder如何放置約束的設計工具。

AutoLayout3.png

3.開始AutoLayout

我會從一個我們常見的用戶注冊頁面的例子講起

AutoLayout4.PNG

我們希望它在橫屏模式下也可以較好地展示出來。

我們在storyBoard中拖拽控件,注意當你拖拽的時候,藍色虛線將會出現。我們應該用這些虛線來做向導。通過preview(點擊show the Assistant Editor,并且切換到preview即可開啟)可以看出沒有AutoLayout的時候控件擺放非常糟糕。

AutoLayout4.png
AutoLayout5.png

首先對于左上方的imageView,我們希望它不管屏幕大小如何,都保持同樣的大小,所以我們需要做的是將鼠標放在改view上,按住control鍵,并垂直拖拽。效果如下:

AutoLayout5.png

我們點擊Height,這樣就規定了它的高度固定不變。同樣的道理,我們點擊Weight,這次你需要水平拖拽。

現在我們觀察preview,你會發現imageview跑到左上角去了。并且imageView會出現橙色的線。為什么呢?因為你的AutoLayout是不完整的,你只規定了高度和寬度,你沒有規定它距離它的父view邊緣的距離是多少。將鼠標放在改view上,按住control鍵,并拖拽至最外面的view上,放開:會看到下面的選項:

AutoLayout6.png

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,放手如下:

AutoLayout7.png

Vertical Spacing:是指兩個view之間的垂直距離固定
Left:是指兩個view左邊對齊
Center X:是指兩個view左邊對齊
Right:是指兩個viewX軸中心對齊

我們需要點擊第一個選項來固定自身和上方imageView之間的距離,然后我們需要固定自身和父view邊緣的距離,所以我們拖拽至父view左右邊緣,如下圖:

AutoLayout8.png
AutoLayout9.png

Leading Space to Superview:view至父view左邊緣長度(前置距離)固定
Trailing Space to Superview:view至父view右邊緣長度(尾隨距離)固定

我們對第二個UITextField以及下面的UIButton進行同樣的操作。

當你對下面的UIButton進行同樣的操作后你會發現仍然出現了表示警告的橙色線,為什么呢?如果你不知道為什么,你可以看到Document Outline那里有一個黃色箭頭,點擊它,你會來到下圖所示:

AutoLayout11.png

它說:你期望的view高度是30,但現在它確實52,你需要修復它

你可能懷疑為什么button沒有Width約束,自動布局是為何知道button有多寬(30)的?

事情是這樣的:button自己是知道自己有多寬。它根據自己的title加上一些padding就行了。如果你為button的title設置更大的字號,它會自動調整它的寬度。

這正是我們熟悉的intrinsic content size。并不是所有的控制器都有這個,但大部分是(UILabel是一個例子)。如果一個視圖可以計算自己理想的大小,那么你就不需要為它特別指定Width或Height約束了。但在我們的例子中,我希望這個button更高啊,那怎么辦?

點擊那個黃色的三角形你會看到:

AutoLayout12.png

Update Frame?不,我們不想它的高度變小。Update Constrains?如果你點擊這個選項的話,你會發現什么變化都沒有,因為在它需要你沒有在Height上設置一個約束條件,也就談不上更新。那我們點擊第三個選項試試。

可以發現警告消失了,那我們點擊這個選項之后,XCode為我們做了什么?

AutoLayout13.png

看這里我們可以發現,UIButton自身增加了一個高度不變的約束條件,所以警告消失了。

我們運行看看效果如何

豎屏:

AutoLayout15.PNG

橫屏:

AutoLayout14.PNG

嗯,豎屏看起來還不錯,橫屏看起來不是太好,UIButton看不見了。這很好理解因為我們固定了控件和父view頂部的距離,但橫屏下高度變小所以UIButton被擠到下面去了。那怎么辦?如果我們固定了控件和父view底部的距離,很有可能會造成image在橫屏模式下被擠到上面去,所以有沒有更好的解決辦法呢。

其實上面講到的這些約束條件也是對象,它們是NSLayoutConstraint對象,所以我們可以在程序運行是動態改變其中的約束條件,如下圖,我們將imageView和父view的垂直距離約束條件拖動到代碼中:

AutoLayout16.png

在ViewController.m中寫下以下代碼:

-(void)viewWillLayoutSubviews{
    if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)){
        self.imageViewToViewSpace.constant = 30;
    }
    else{
        self.imageViewToViewSpace.constant = 82;
    }
}

效果看上去還不錯:

AutoLayout17.PNG

4.稍微復雜點的AutoLayout:

我們再將上面那個例子變得稍微復雜一點,把下面的Button變為3個button,每個等寬。

我們首先為button1增加上左右3個距離寬度約束條件:

為button2增加與button1等y軸,與button3距離約束,將button1與button2的距離和button2和button3的距離都設置為8:

為button3增加與button2等y軸,與父view的Trailing space:

屏幕快照 2015-03-11 下午5.19.06.png

看起來好多警告?恩下面是重點,我們把button1,button2,button3,設置為同樣高度同樣寬度,警告就消失了。

現在運行效果如下:

5.SizeClass

我們上面使用了NSLayoutConstraint的IBOutlet對象,所以我們可以在程序運行是動態改變其中的約束條件,不過有另外一種更優雅的方式來實現上面的效果:使用SizeClass。

隨著iPhone6/iPhone6 Plus的發布,現在蘋果生態圈中的設備尺寸也已經變得種類繁多了。想必蘋果也意識到這一點。都知道蘋果是以化繁為簡的設計哲學深入人心的,這次再一次證明了。SizeClass是對設備尺寸的一個抽象概念,現在任何設備的 長、寬 被簡潔地分為三種情況:普通 (Regular) 、緊密 (Compact)和任意(Any) ,這樣,根據長和寬不同的搭配就能產生 3*3=9 種不同尺寸。下圖展示個每種情況對應的設備。

1411722627166330.png

我們可以在不同的屏幕尺寸下使用不同的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的高度。

IMG_0920.PNG

我們新建一個UITableViewController的子類,在我們的storyBoard中添加一個TableViewController,并將它的自定義類設置為DynamicCellHeightViewController。

AutoLayout1.png

在我們的cell上添加如下imageView和Label兩個控件:

AutoLayout4.png

新建自定義的cell類CustomTableViewCell并將storyBoard中的cell關聯至該類。

AutoLayout7.png

并將UILabel的屬性Lines設為了0以表示顯示多行。將cell的Identifier設置為"cell"。

讓我們給這些view一點約束。在上一篇文章你已經知道了通過按住ctrl在兩個view之間拖拽增加約束的方式,此外,我們還有其他兩種方式:用Editor\Pin和Align菜單:

AutoLayout2.png

還有在Interface Builder窗口的底部有一行這樣的按鈕:

AutoLayout3.png

從左到右分別是:對齊(Align),固定(Pin),解決自動布局問題(Resolve Auto Layout Issues)和重定義尺寸(Resizing Behavior)。前三個按鈕魚Editor菜單中的對應項有一致的功能。Resizing Behavior按鈕允許你在重新設置view的尺寸的時候,改變已經添加的約束。

頂部的Spacing to nearest neighbor可以添加上下左右四個約束條件,點擊者4個T字架,它們就會變成實體的紅色:

AutoLayout5.png

如上圖所示,我們為imageView增加4個約束條件。同理我們為label增加4個約束條件。

AutoLayout6.png

好了,我們已經完成了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;
}

運行效果如下:

AutoLayout8.PNG

恩,好像哪里不對,計算cell高度時應該考慮頭像的高度。

如下圖,我們為頭像和cell之間添加一個約束條件,并在右面板中將Constant設置為>=10,這樣cell的最小高度也是頭像高度位置加上10了。

AutoLayout9.png

運行效果如下:

AutoLayout10.PNG

如果不用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:)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容