目前iOS
開發中大多數頁面都已經開始使用Interface Builder
的方式進行UI
開發了,但是在一些變化比較複雜的頁面,還是需要通過程式碼來進行UI
開發的。而且有很多比較老的專案,本身就還在採用純程式碼的方式進行開發。
而現在iPhone
和iPad
螢幕尺寸越來越多,雖然開發者只需要根據螢幕點進行開發,而不需要基於畫素點進行UI
開發。但如果在專案中根據不同螢幕尺寸進行各種判斷,寫死座標的話,這樣開發起來是很吃力的。
所以一般用純程式碼開發UI
的話,一般都是配合一些自動化佈局的框架進行螢幕適配。蘋果為我們提供的適配框架有:VFL
、UIViewAutoresizing
、Auto Layout
、Size Classes
等。
其中Auto Layout
是使用頻率最高的佈局框架,但是其也有弊端。就是在使用UILayoutConstraint
的時候,會發現程式碼量很多,而且大多都是重複性的程式碼,以至於好多人都不想用這個框架。
後來Github
上的出現了基於UILayoutConstraint
封裝的第三方佈局框架Masonry
,Masonry
使用起來非常方便,本篇文章就詳細講一下Masonry
的使用。
Masonry介紹
這篇文章只是簡單介紹Masonry
,以及Masonry
的使用,並且會舉一些例子出來。但並不會涉及到Masonry
的內部實現,以後會專門寫篇文章來介紹其內部實現原理,包括順便講一下鏈式語法。
什麼是Masonry
Masonry
是一個對系統NSLayoutConstraint
進行封裝的第三方自動佈局框架,採用鏈式程式設計的方式提供給開發者API
。系統AutoLayout
支援的操作,Masonry
都支援,相比系統API
功能來說,Masonry
是有過之而無不及。
Masonry
採取了鏈式程式設計的方式,程式碼理解起來非常清晰易懂,而且寫完之後程式碼量看起來非常少。之前用NSLayoutConstraint
寫很多程式碼才能實現的佈局,用Masonry
最少一行程式碼就可以搞定。下面看到Masonry
的程式碼就會發現,太簡單易懂了。
Masonry
是同時支援Mac
和iOS
兩個平臺的,在這兩個平臺上都可以使用Masonry
進行自動佈局。我們可以從MASUtilities.h
檔案中,看到下面的定義,這就是Masonry
通過巨集定義的方式,區分兩個平臺獨有的一些關鍵字。
1 2 3 4 5 6 7 8 9 |
#if TARGET_OS_IPHONE #import #define MAS_VIEW UIView #define MASEdgeInsets UIEdgeInsets #elif TARGET_OS_MAC #import #define MAS_VIEW NSView #define MASEdgeInsets NSEdgeInsets #endif |
Github地址:
https://github.com/SnapKit/Masonry
整合方式
Masonry
支援CocoaPods
,可以直接通過podfile
檔案進行整合,需要在CocoaPods
中新增下面程式碼:
1 |
pod 'Masonry' |
Masonry學習建議
在UI
開發中,純程式碼和Interface Builder
我都是用過的,在開發過程中也積累了一些經驗。對於初學者學習純程式碼AutoLayout
,我建議還是先學會Interface Builder
方式的AutoLayout
,領悟蘋果對自動佈局的規則和思想,然後再把這套思想巢狀在純程式碼上。這樣學習起來更好入手,也可以避免踩好多坑。
在專案中設定的AutoLayout
約束,起到對檢視佈局的標記作用。設定好約束之後,程式執行過程中建立檢視時,會根據設定好的約束計算frame
,並渲染到檢視上。
所以在純程式碼情況下,檢視設定的約束是否正確,要以執行之後顯示的結果和列印的log
為準。
Masonry中的坑
在使用Masonry
進行約束時,有一些是需要注意的。
- 在使用
Masonry
新增約束之前,需要在addSubview
之後才能使用,否則會導致崩潰。 - 在新增約束時初學者經常會出現一些錯誤,約束出現問題的原因一般就是兩種:約束衝突和缺少約束。對於這兩種問題,可以通過除錯和
log
排查。 - 之前使用
Interface Builder
新增約束,如果約束有錯誤直接就可以看出來,並且會以紅色或者黃色警告體現出來。而Masonry
則不會直觀的體現出來,而是以執行過程中崩潰或者列印異常log
體現,所以這也是手寫程式碼進行AutoLayout
的一個缺點。
這個問題只能通過多敲程式碼,積攢純程式碼進行AutoLayout
的經驗,慢慢就用起來越來越得心應手了。
Masonry基礎使用
Masonry基礎API
1 2 3 4 5 6 7 8 9 |
mas_makeConstraints() 新增約束 mas_remakeConstraints() 移除之前的約束,重新新增新的約束 mas_updateConstraints() 更新約束 equalTo() 引數是物件型別,一般是檢視物件或者mas_width這樣的座標系物件 mas_equalTo() 和上面功能相同,引數可以傳遞基礎資料型別物件,可以理解為比上面的API更強大 width() 用來表示寬度,例如代表view的寬度 mas_width() 用來獲取寬度的值。和上面的區別在於,一個代表某個座標系物件,一個用來獲取座標系物件的值 |
Auto Boxing
上面例如equalTo
或者width
這樣的,有時候需要涉及到使用mas_
字首,這在開發中需要注意作區分。
如果在當前類引入#import "Masonry.h"
之前,用下面兩種巨集定義宣告一下,就不需要區分mas_
字首。
1 2 3 4 |
// 定義這個常量,就可以不用在開發過程中使用"mas_"字首。 #define MAS_SHORTHAND // 定義這個常量,就可以讓Masonry幫我們自動把基礎資料型別的資料,自動裝箱為物件型別。 #define MAS_SHORTHAND_GLOBALS |
修飾語句
Masonry
為了讓程式碼使用和閱讀更容易理解,所以直接通過點語法就可以呼叫,還新增了and
和with
兩個方法。這兩個方法內部實際上什麼都沒幹,只是在內部將self
直接返回,功能就是為了更加方便閱讀,對程式碼執行沒有實際作用。
例如下面的例子:
1 |
make.top.and.bottom.equalTo(self.containerView).with.offset(padding); |
其內部程式碼實現,實際上就是直接將self
返回。
1 2 3 |
- (MASConstraint *)with { return self; } |
更新約束和佈局
關於更新約束佈局相關的API
,主要用以下四個API
:
1 2 3 4 |
- (void)updateConstraintsIfNeeded 呼叫此方法,如果有標記為需要重新佈局的約束,則立即進行重新佈局,內部會呼叫updateConstraints方法 - (void)updateConstraints 重寫此方法,內部實現自定義佈局過程 - (BOOL)needsUpdateConstraints 當前是否需要重新佈局,內部會判斷當前有沒有被標記的約束 - (void)setNeedsUpdateConstraints 標記需要進行重新佈局 |
關於UIView
重新佈局相關的API
,主要用以下三個API
:
1 2 3 |
- (void)setNeedsLayout 標記為需要重新佈局 - (void)layoutIfNeeded 檢視當前檢視是否被標記需要重新佈局,有則在內部呼叫layoutSubviews方法進行重新佈局 - (void)layoutSubviews 重寫當前方法,在內部完成重新佈局操作 |
Masonry示例程式碼
1 2 3 |
Masonry本質上就是對系統AutoLayout進行的封裝,包括裡面很多的API,都是對系統API進行了一次二次包裝。 typedef NS_OPTIONS(NSInteger, MASAttribute) { MASAttributeLeft = 1 |
常用方法
設定內邊距
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** 設定yellow檢視和self.view等大,並且有10的內邊距。 注意根據UIView的座標系,下面right和bottom進行了取反。所以不能寫成下面這樣,否則right、bottom這兩個方向會出現問題。 make.edges.equalTo(self.view).with.offset(10); 除了下面例子中的offset()方法,還有針對不同座標系的centerOffset()、sizeOffset()、valueOffset()之類的方法。 */ [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).with.offset(10); make.top.equalTo(self.view).with.offset(10); make.right.equalTo(self.view).with.offset(-10); make.bottom.equalTo(self.view).with.offset(-10); }]; |
通過insets簡化設定內邊距的方式
1 2 3 4 5 |
// 下面的方法和上面例子等價,區別在於使用insets()方法。 [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { // 下、右不需要寫負號,insets方法中已經為我們做了取反的操作了。 make.edges.equalTo(self.view).with.insets(UIEdgeInsetsMake(10, 10, 10, 10)); }]; |
更新約束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 設定greenView的center和size,這樣就可以達到簡單進行約束的目的 [self.greenView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); // 這裡通過mas_equalTo給size設定了基礎資料型別的引數,引數為CGSize的結構體 make.size.mas_equalTo(CGSizeMake(300, 300)); }]; // 為了更清楚的看出約束變化的效果,在顯示兩秒後更新約束。 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.greenView mas_updateConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.view).offset(100); make.size.mas_equalTo(CGSizeMake(100, 100)); }]; }); |
大於等於和小於等於某個值的約束
1 2 3 4 5 6 7 8 9 |
[self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); // 設定寬度小於等於200 make.width.lessThanOrEqualTo(@200); // 設定高度大於等於10 make.height.greaterThanOrEqualTo(@(10)); }]; self.textLabel.text = @"這是測試的字串。能看到1、2、3個步驟,第一步當然是上傳照片了,要上傳正面近照哦。上傳後,網站會自動識別你的面部,如果覺得識別的不準,你還可以手動修改一下。左邊可以看到16項修改引數,最上面是整體修改,你也可以根據自己的意願單獨修改某項,將滑鼠放到選項上面,右邊的預覽圖會顯示相應的位置。"; |
textLabel
只需要設定一個屬性即可
1 |
self.textLabel.numberOfLines = 0; |
使用基礎資料型別當做引數
1 2 3 4 5 6 7 8 9 10 11 12 |
/** 如果想使用基礎資料型別當做引數,Masonry為我們提供了"mas_xx"格式的巨集定義。 這些巨集定義會將傳入的基礎資料型別轉換為NSNumber型別,這個過程叫做封箱(Auto Boxing)。 "mas_xx"開頭的巨集定義,內部都是通過MASBoxValue()函式實現的。 這樣的巨集定義主要有四個,分別是mas_equalTo()、mas_offset()和大於等於、小於等於四個。 */ [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.width.mas_equalTo(100); make.height.mas_equalTo(100); }]; |
設定約束優先順序
1 2 3 4 5 6 7 8 9 10 11 |
/** Masonry為我們提供了三個預設的方法,priorityLow()、priorityMedium()、priorityHigh(),這三個方法內部對應著不同的預設優先順序。 除了這三個方法,我們也可以自己設定優先順序的值,可以通過priority()方法來設定。 */ [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.width.equalTo(self.view).priorityLow(); make.width.mas_equalTo(20).priorityHigh(); make.height.equalTo(self.view).priority(200); make.height.mas_equalTo(100).priority(1000); }]; |
1 2 3 4 5 6 |
Masonry也幫我們定義好了一些預設的優先順序常量,分別對應著不同的數值,優先順序最大數值是1000。 static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired; static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh; static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 500; static const MASLayoutPriority MASLayoutPriorityDefaultLow = UILayoutPriorityDefaultLow; static const MASLayoutPriority MASLayoutPriorityFittingSizeLevel = UILayoutPriorityFittingSizeLevel; |
設定約束比例
1 2 3 4 5 6 |
// 設定當前約束值乘以多少,例如這個例子是redView的寬度是self.view寬度的0.2倍。 [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.height.mas_equalTo(30); make.width.equalTo(self.view).multipliedBy(0.2); }]; |
小練習
子檢視等高練習
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** 下面的例子是通過給equalTo()方法傳入一個陣列,設定陣列中子檢視及當前make對應的檢視之間等高。 需要注意的是,下面block中設定邊距的時候,應該用insets來設定,而不是用offset。 因為用offset設定right和bottom的邊距時,這兩個值應該是負數,所以如果通過offset來統一設定值會有問題。 */ CGFloat padding = LXZViewPadding; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.top.equalTo(self.view).insets(UIEdgeInsetsMake(padding, padding, 0, padding)); make.bottom.equalTo(self.blueView.mas_top).offset(-padding); }]; [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, 0, padding)); make.bottom.equalTo(self.yellowView.mas_top).offset(-padding); }]; /** 下面設定make.height的陣列是關鍵,通過這個陣列可以設定這三個檢視高度相等。其他例如寬度之類的,也是類似的方式。 */ [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.bottom.equalTo(self.view).insets(UIEdgeInsetsMake(0, padding, padding, padding)); make.height.equalTo(@[self.blueView, self.redView]); }]; |
子檢視垂直居中練習
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** 要求:(這個例子是在其他人部落格裡看到的,然後按照要求自己寫了下面這段程式碼) 兩個檢視相對於父檢視垂直居中,並且兩個檢視以及父檢視之間的邊距均為10,高度為150,兩個檢視寬度相等。 */ CGFloat padding = 10.f; [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(self.view); make.left.equalTo(self.view).mas_offset(padding); make.right.equalTo(self.redView.mas_left).mas_offset(-padding); make.width.equalTo(self.redView); make.height.mas_equalTo(150); }]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(self.view); make.right.equalTo(self.view).mas_offset(-padding); make.width.equalTo(self.blueView); make.height.mas_equalTo(150); }]; |
UITableView動態Cell高度
在iOS
UI
開發過程中,UITableView
的動態Cell
高度一直都是個問題。實現這樣的需求,實現方式有很多種,只是實現起來複雜程度和效能的區別。
在不考慮效能的情況下,tableView
動態Cell
高度,可以採取估算高度的方式。如果通過估算高度的方式實現的話,無論是純程式碼還是Interface Builder
,都只需要兩行程式碼就可以完成Cell
自動高度適配。
實現方式:
需要設定tableView
的rowHeight
屬性,這裡設定為自動高度,告訴系統Cell
的高度是不固定的,需要系統幫我們進行計算。然後設定tableView
的estimatedRowHeight
屬性,設定一個估計的高度。(我這裡用的代理方法,實際上都一樣)
原理:
這樣的話,在tableView
被建立之後,系統會根據estimatedRowHeight
屬性設定的值,為tableView
設定一個估計的值。然後在Cell
顯示的時候再獲取Cell
的高度,並重新整理tableView
的contentSize
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
- (void)tableViewConstraints { [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.dataList.count; } - (MasonryTableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MasonryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LXZTableViewCellIdentifier]; [cell reloadViewWithText:self.dataList[indexPath.row]]; return cell; } // 需要注意的是,這個代理方法和直接返回當前Cell高度的代理方法並不一樣。 // 這個代理方法會將當前所有Cell的高度都預估出來,而不是隻計算顯示的Cell,所以這種方式對效能消耗還是很大的。 // 所以通過設定estimatedRowHeight屬性的方式,和這種代理方法的方式,最後效能消耗都是一樣的。 - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { return 50.f; } - (UITableView *)tableView { if (!_tableView) { _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _tableView.delegate = self; _tableView.dataSource = self; // 設定tableView自動高度 _tableView.rowHeight = UITableViewAutomaticDimension; [_tableView registerClass:[MasonryTableViewCell class] forCellReuseIdentifier:LXZTableViewCellIdentifier]; [self.view addSubview:_tableView]; } return _tableView; } |
UIScrollView自動佈局
之前聽很多人說過UIScrollView
很麻煩,然而我並沒有感覺到有多麻煩(並非裝逼)。我感覺說麻煩的人可能根本就沒試過吧,只是覺得很麻煩而已。
我這裡就講一下兩種進行UIScrollView
自動佈局的方案,並且會講一下自動佈局的技巧,只要掌握技巧,佈局其實很簡單。
佈局小技巧:
給UIScrollView
新增的約束是定義其frame
,設定contentSize
是定義其內部大小。UIScrollView
進行addSubview
操作,都是將其子檢視新增到contentView
上。
所以,新增到UIScrollView
上的子檢視,對UIScrollView
新增的約束都是作用於contentView
上的。只需要按照這樣的思路給UIScrollView
設定約束,就可以掌握設定約束的技巧了。
提前設定contentSize
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 提前設定好UIScrollView的contentSize,並設定UIScrollView自身的約束 self.scrollView.contentSize = CGSizeMake(1000, 1000); [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; // 雖然redView的get方法內部已經執行過addSubview操作,但是UIView始終以最後一次新增的父檢視為準,也就是redView始終是在最後一次新增的父檢視上。 [self.scrollView addSubview:self.redView]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.top.equalTo(self.scrollView); make.width.height.mas_equalTo(200); }]; [self.scrollView addSubview:self.blueView]; [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.redView.mas_right); make.top.equalTo(self.scrollView); make.width.height.equalTo(self.redView); }]; [self.scrollView addSubview:self.greenView]; [self.greenView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.scrollView); make.top.equalTo(self.redView.mas_bottom); make.width.height.equalTo(self.redView); }]; |
自動contentSize
上面的例子是提前設定好UIScrollView
的contentSize
的內部size
,然後直接向裡面addSubview
。但是這有個要求就是,需要提前知道contentSize
的大小,不然沒法設定。
這個例子中將會展示動態改變contentSize
的大小,內部檢視有多少contentSize
就自動擴充到多大。
這種方式的實現,主要是依賴於建立一個containerView
內容檢視,並新增到UIScrollView
上作為子檢視。UIScrollView
原來的子檢視都新增到containerView
上,並且和這個檢視設定約束。
因為對UIScrollView
進行addSubview
操作的時候,本質上是往其contentView
上新增。也就是containerView
的父檢視是contentView
,通過containerView
撐起contentView
檢視的大小,以此來實現動態改變contentSize
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 在進行約束的時候,要對containerView的上下左右都新增和子檢視的約束,以便確認containerView的邊界區域。 [self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; CGFloat padding = LXZViewPadding; [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.scrollView).insets(UIEdgeInsetsMake(padding, padding, padding, padding)); }]; [self.containerView addSubview:self.greenView]; [self.greenView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.equalTo(self.containerView).offset(padding); make.size.mas_equalTo(CGSizeMake(250, 250)); }]; [self.containerView addSubview:self.redView]; [self.redView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.containerView).offset(padding); make.left.equalTo(self.greenView.mas_right).offset(padding); make.size.equalTo(self.greenView); make.right.equalTo(self.containerView).offset(-padding); }]; [self.containerView addSubview:self.yellowView]; [self.yellowView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.containerView).offset(padding); make.top.equalTo(self.greenView.mas_bottom).offset(padding); make.size.equalTo(self.greenView); make.bottom.equalTo(self.containerView).offset(-padding); }]; |