<簡書 — 劉小壯> http://www.jianshu.com/p/ea74b230c70dgit
目前
iOS
開發中大多數頁面都已經開始使用Interface Builder
的方式進行UI
開發了,可是在一些變化比較複雜的頁面,仍是須要經過代碼來進行UI
開發的。並且有不少比較老的項目,自己就還在採用純代碼的方式進行開發。github而如今
iPhone
和iPad
屏幕尺寸愈來愈多,雖然開發者只須要根據屏幕點進行開發,而不須要基於像素點進行UI
開發。但若是在項目中根據不一樣屏幕尺寸進行各類判斷,寫死座標的話,這樣開發起來是很吃力的。objective-c因此通常用純代碼開發
UI
的話,通常都是配合一些自動化佈局的框架進行屏幕適配。蘋果爲咱們提供的適配框架有:VFL
、UIViewAutoresizing
、Auto Layout
、Size Classes
等。編程其中
Auto Layout
是使用頻率最高的佈局框架,可是其也有弊端。就是在使用NSLayoutConstraint
的時候,會發現代碼量不少,並且大多都是重複性的代碼,以致於好多人都不想用這個框架。數組後來
Github
上的出現了基於NSLayoutConstraint
封裝的第三方佈局框架Masonry
,Masonry
使用起來很是方便,本篇文章就詳細講一下Masonry
的使用。框架
這篇文章只是簡單介紹Masonry
,以及Masonry
的使用,而且會舉一些例子出來。但並不會涉及到Masonry
的內部實現,之後會專門寫篇文章來介紹其內部實現原理,包括順便講一下鏈式語法。less
Masonry
是一個對系統NSLayoutConstraint
進行封裝的第三方自動佈局框架,採用鏈式編程的方式提供給開發者API
。系統AutoLayout
支持的操做,Masonry
都支持,相比系統API
功能來講,Masonry
是有過之而無不及。函數
Masonry
採起了鏈式編程的方式,代碼理解起來很是清晰易懂,並且寫完以後代碼量看起來很是少。以前用NSLayoutConstraint
寫不少代碼才能實現的佈局,用Masonry
最少一行代碼就能夠搞定。下面看到Masonry
的代碼就會發現,太簡單易懂了。佈局
Masonry
是同時支持Mac
和iOS
兩個平臺的,在這兩個平臺上均可以使用Masonry
進行自動佈局。咱們能夠從MASUtilities.h
文件中,看到下面的定義,這就是Masonry
經過宏定義的方式,區分兩個平臺獨有的一些關鍵字。性能
#if TARGET_OS_IPHONE #import <UIKit/UIKit.h> #define MAS_VIEW UIView #define MASEdgeInsets UIEdgeInsets #elif TARGET_OS_MAC #import <AppKit/AppKit.h> #define MAS_VIEW NSView #define MASEdgeInsets NSEdgeInsets #endif
Github地址: https://github.com/SnapKit/Masonry
Masonry
支持CocoaPods
,能夠直接經過podfile
文件進行集成,須要在CocoaPods
中添加下面代碼:
pod 'Masonry'
在UI
開發中,純代碼和Interface Builder
我都是用過的,在開發過程當中也積累了一些經驗。對於初學者學習純代碼AutoLayout
,我建議仍是先學會Interface Builder
方式的AutoLayout
,領悟蘋果對自動佈局的規則和思想,而後再把這套思想嵌套在純代碼上。這樣學習起來更好入手,也能夠避免踩好多坑。
在項目中設置的AutoLayout
約束,起到對視圖佈局的標記做用。設置好約束以後,程序運行過程當中建立視圖時,會根據設置好的約束計算frame
,並渲染到視圖上。
因此在純代碼狀況下,視圖設置的約束是否正確,要以運行以後顯示的結果和打印的log
爲準。
在使用Masonry
進行約束時,有一些是須要注意的。
Masonry
添加約束以前,須要在addSubview
以後才能使用,不然會致使崩潰。log
排查。Interface Builder
添加約束,若是約束有錯誤直接就能夠看出來,而且會以紅色或者黃色警告體現出來。而Masonry
則不會直觀的體現出來,而是以運行過程當中崩潰或者打印異常log
體現,因此這也是手寫代碼進行AutoLayout
的一個缺點。 這個問題只能經過多敲代碼,積攢純代碼進行AutoLayout
的經驗,慢慢就用起來愈來愈駕輕就熟了。mas_makeConstraints() 添加約束 mas_remakeConstraints() 移除以前的約束,從新添加新的約束 mas_updateConstraints() 更新約束,寫哪條更新哪條,其餘約束不變 equalTo() 參數是對象類型,通常是視圖對象或者mas_width這樣的座標系對象 mas_equalTo() 和上面功能相同,參數能夠傳遞基礎數據類型對象,能夠理解爲比上面的API更強大 width() 用來表示寬度,例如表明view的寬度 mas_width() 用來獲取寬度的值。和上面的區別在於,一個表明某個座標系對象,一個用來獲取座標系對象的值
上面例如equalTo
或者width
這樣的,有時候須要涉及到使用mas_
前綴,這在開發中須要注意做區分。 若是在當前類引入#import "Masonry.h"
以前,用下面兩種宏定義聲明一下,就不須要區分mas_
前綴。
// 定義這個常量,就能夠不用在開發過程當中使用"mas_"前綴。 #define MAS_SHORTHAND // 定義這個常量,就可讓Masonry幫咱們自動把基礎數據類型的數據,自動裝箱爲對象類型。 #define MAS_SHORTHAND_GLOBALS
Masonry
爲了讓代碼使用和閱讀更容易理解,因此直接經過點語法就能夠調用,還添加了and
和with
兩個方法。這兩個方法內部實際上什麼都沒幹,只是在內部將self
直接返回,功能就是爲了更加方便閱讀,對代碼執行沒有實際做用。 例以下面的例子:
make.top.and.bottom.equalTo(self.containerView).with.offset(padding);
其內部代碼實現,實際上就是直接將self
返回。
- (MASConstraint *)with { return self; }
關於更新約束佈局相關的API
,主要用如下四個API
:
- (void)updateConstraintsIfNeeded 調用此方法,若是有標記爲須要從新佈局的約束,則當即進行從新佈局,內部會調用updateConstraints方法 - (void)updateConstraints 重寫此方法,內部實現自定義佈局過程 - (BOOL)needsUpdateConstraints 當前是否須要從新佈局,內部會判斷當前有沒有被標記的約束 - (void)setNeedsUpdateConstraints 標記須要進行從新佈局
關於UIView
從新佈局相關的API
,主要用如下三個API
:
- (void)setNeedsLayout 標記爲須要從新佈局 - (void)layoutIfNeeded 查看當前視圖是否被標記須要從新佈局,有則在內部調用layoutSubviews方法進行從新佈局 - (void)layoutSubviews 重寫當前方法,在內部完成從新佈局操做
Masonry本質上就是對系統AutoLayout進行的封裝,包括裏面不少的API,都是對系統API進行了一次二次包裝。 typedef NS_OPTIONS(NSInteger, MASAttribute) { MASAttributeLeft = 1 << NSLayoutAttributeLeft, MASAttributeRight = 1 << NSLayoutAttributeRight, MASAttributeTop = 1 << NSLayoutAttributeTop, MASAttributeBottom = 1 << NSLayoutAttributeBottom, MASAttributeLeading = 1 << NSLayoutAttributeLeading, MASAttributeTrailing = 1 << NSLayoutAttributeTrailing, MASAttributeWidth = 1 << NSLayoutAttributeWidth, MASAttributeHeight = 1 << NSLayoutAttributeHeight, MASAttributeCenterX = 1 << NSLayoutAttributeCenterX, MASAttributeCenterY = 1 << NSLayoutAttributeCenterY, MASAttributeBaseline = 1 << NSLayoutAttributeBaseline, };
/** 設置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()方法。 [self.blueView mas_makeConstraints:^(MASConstraintMaker *make) { // 下、右不須要寫負號,insets方法中已經爲咱們作了取反的操做了。 make.edges.equalTo(self.view).with.insets(UIEdgeInsetsMake(10, 10, 10, 10)); }];
// 設置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(), ^{ // 指定更新size,其餘約束不變。 [self.greenView mas_updateConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(100, 100)); }]; });
[self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); // 設置寬度小於等於200 make.width.lessThanOrEqualTo(@200); // 設置高度大於等於10 make.height.greaterThanOrEqualTo(@(10)); }]; self.textLabel.text = @"這是測試的字符串。能看到一、二、3個步驟,第一步固然是上傳照片了,要上傳正面近照哦。上傳後,網站會自動識別你的面部,若是以爲識別的不許,你還能夠手動修改一下。左邊能夠看到16項修改參數,最上面是總體修改,你也能夠根據本身的意願單獨修改某項,將鼠標放到選項上面,右邊的預覽圖會顯示相應的位置。";
textLabel
只須要設置一個屬性便可
self.textLabel.numberOfLines = 0;
/** 若是想使用基礎數據類型當作參數,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); }];
/** 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); }];
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;
// 設置當前約束值乘以多少,例如這個例子是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); }];
/** 下面的例子是經過給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]); }];
/** 要求:(這個例子是在其餘人博客裏看到的,而後按照要求本身寫了下面這段代碼) 兩個視圖相對於父視圖垂直居中,而且兩個視圖以及父視圖之間的邊距均爲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); }];
在iOS
UI
開發過程當中,UITableView
的動態Cell
高度一直都是個問題。實現這樣的需求,實現方式有不少種,只是實現起來複雜程度和性能的區別。
在不考慮性能的狀況下,tableView
動態Cell
高度,能夠採起估算高度的方式。若是經過估算高度的方式實現的話,不管是純代碼仍是Interface Builder
,都只須要兩行代碼就能夠完成Cell
自動高度適配。
實現方式: 須要設置tableView
的rowHeight
屬性,這裏設置爲自動高度,告訴系統Cell
的高度是不固定的,須要系統幫咱們進行計算。而後設置tableView
的estimatedRowHeight
屬性,設置一個估計的高度。(我這裏用的代理方法,實際上都同樣)
原理: 這樣的話,在tableView
被建立以後,系統會根據estimatedRowHeight
屬性設置的值,爲tableView
設置一個估計的值。而後在Cell
顯示的時候再獲取Cell
的高度,並刷新tableView
的contentSize
。
實現代碼: UITableView部分
- (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; }
UITableViewCell部分
// 自定義了一個UIImageView和UILabel控件,而且經過Masonry進行約束。 [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(40, 40)); make.top.left.equalTo(self.contentView).mas_offset(CellPadding); }]; [self.detailLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.avatarImageView.mas_right).mas_offset(CellPadding); make.top.equalTo(self.contentView).mas_offset(CellPadding); make.right.bottom.equalTo(self.contentView).mas_offset(-CellPadding); make.height.greaterThanOrEqualTo(@30); }];
以前聽不少人說過UIScrollView
很麻煩,然而我並無感受到有多麻煩(並不是裝逼)。我感受說麻煩的人可能根本就沒試過吧,只是以爲很麻煩而已。 我這裏就講一下兩種進行UIScrollView
自動佈局的方案,而且會講一下自動佈局的技巧,只要掌握技巧,佈局其實很簡單。
佈局小技巧: 給
UIScrollView
添加的約束是定義其frame
,設置contentSize
是定義其內部大小。UIScrollView
進行addSubview
操做,都是將其子視圖添加到contentView
上。 因此,添加到UIScrollView
上的子視圖,對UIScrollView
添加的約束都是做用於contentView
上的。只須要按照這樣的思路給UIScrollView
設置約束,就能夠掌握設置約束的技巧了。
// 提早設置好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); }];
上面的例子是提早設置好UIScrollView
的contentSize
的內部size
,而後直接向裏面addSubview
。可是這有個要求就是,須要提早知道contentSize
的大小,否則無法設置。 這個例子中將會展現動態改變contentSize
的大小,內部視圖有多少contentSize
就自動擴充到多大。
這種方式的實現,主要是依賴於建立一個containerView
內容視圖,並添加到UIScrollView
上做爲子視圖。UIScrollView
原來的子視圖都添加到containerView
上,而且和這個視圖設置約束。 由於對UIScrollView
進行addSubview
操做的時候,本質上是往其contentView
上添加。也就是containerView
的父視圖是contentView
,經過containerView
撐起contentView
視圖的大小,以此來實現動態改變contentSize
。
// 在進行約束的時候,要對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); }];