iOS自動佈局框架 - Masonry詳解

weixin_33727510發表於2017-01-08
該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> http://www.jianshu.com/p/ea74b230c70d


目前iOS開發中大多數頁面都已經開始使用Interface Builder的方式進行UI開發了,但是在一些變化比較複雜的頁面,還是需要通過程式碼來進行UI開發的。而且有很多比較老的專案,本身就還在採用純程式碼的方式進行開發。

而現在iPhoneiPad螢幕尺寸越來越多,雖然開發者只需要根據螢幕點進行開發,而不需要基於畫素點進行UI開發。但如果在專案中根據不同螢幕尺寸進行各種判斷,寫死座標的話,這樣開發起來是很吃力的。

所以一般用純程式碼開發UI的話,一般都是配合一些自動化佈局的框架進行螢幕適配。蘋果為我們提供的適配框架有:VFLUIViewAutoresizingAuto LayoutSize Classes等。

其中Auto Layout是使用頻率最高的佈局框架,但是其也有弊端。就是在使用NSLayoutConstraint的時候,會發現程式碼量很多,而且大多都是重複性的程式碼,以至於好多人都不想用這個框架。

後來Github上的出現了基於NSLayoutConstraint封裝的第三方佈局框架MasonryMasonry使用起來非常方便,本篇文章就詳細講一下Masonry的使用。


270478-17b97c6718048712.jpg
佔點陣圖

Masonry介紹

這篇文章只是簡單介紹Masonry,以及Masonry的使用,並且會舉一些例子出來。但並不會涉及到Masonry的內部實現,以後會專門寫篇文章來介紹其內部實現原理,包括順便講一下鏈式語法。

什麼是Masonry

Masonry是一個對系統NSLayoutConstraint進行封裝的第三方自動佈局框架,採用鏈式程式設計的方式提供給開發者API。系統AutoLayout支援的操作,Masonry都支援,相比系統API功能來說,Masonry是有過之而無不及。

Masonry採取了鏈式程式設計的方式,程式碼理解起來非常清晰易懂,而且寫完之後程式碼量看起來非常少。之前用NSLayoutConstraint寫很多程式碼才能實現的佈局,用Masonry最少一行程式碼就可以搞定。下面看到Masonry的程式碼就會發現,太簡單易懂了。

Masonry是同時支援MaciOS兩個平臺的,在這兩個平臺上都可以使用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'

Masonry學習建議

UI開發中,純程式碼和Interface Builder我都是用過的,在開發過程中也積累了一些經驗。對於初學者學習純程式碼AutoLayout,我建議還是先學會Interface Builder方式的AutoLayout,領悟蘋果對自動佈局的規則和思想,然後再把這套思想巢狀在純程式碼上。這樣學習起來更好入手,也可以避免踩好多坑。

在專案中設定的AutoLayout約束,起到對檢視佈局的標記作用。設定好約束之後,程式執行過程中建立檢視時,會根據設定好的約束計算frame,並渲染到檢視上。

所以在純程式碼情況下,檢視設定的約束是否正確,要以執行之後顯示的結果和列印的log為準。

Masonry中的坑

在使用Masonry進行約束時,有一些是需要注意的。

  1. 在使用Masonry新增約束之前,需要在addSubview之後才能使用,否則會導致崩潰。
  2. 在新增約束時初學者經常會出現一些錯誤,約束出現問題的原因一般就是兩種:約束衝突和缺少約束。對於這兩種問題,可以通過除錯和log排查。
  3. 之前使用Interface Builder新增約束,如果約束有錯誤直接就可以看出來,並且會以紅色或者黃色警告體現出來。而Masonry則不會直觀的體現出來,而是以執行過程中崩潰或者列印異常log體現,所以這也是手寫程式碼進行AutoLayout的一個缺點。
    這個問題只能通過多敲程式碼,積攢純程式碼進行AutoLayout的經驗,慢慢就用起來越來越得心應手了。

Masonry基礎使用

Masonry基礎API

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_字首。

// 定義這個常量,就可以不用在開發過程中使用"mas_"字首。
#define MAS_SHORTHAND
// 定義這個常量,就可以讓Masonry幫我們自動把基礎資料型別的資料,自動裝箱為物件型別。
#define MAS_SHORTHAND_GLOBALS

修飾語句

Masonry為了讓程式碼使用和閱讀更容易理解,所以直接通過點語法就可以呼叫,還新增了andwith兩個方法。這兩個方法內部實際上什麼都沒幹,只是在內部將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示例程式碼

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簡化設定內邊距的方式
// 下面的方法和上面例子等價,區別在於使用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 = @"這是測試的字串。能看到1、2、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);
}];

UITableView動態Cell高度

iOS UI開發過程中,UITableView的動態Cell高度一直都是個問題。實現這樣的需求,實現方式有很多種,只是實現起來複雜程度和效能的區別。

在不考慮效能的情況下,tableView動態Cell高度,可以採取估算高度的方式。如果通過估算高度的方式實現的話,無論是純程式碼還是Interface Builder,都只需要兩行程式碼就可以完成Cell自動高度適配。

實現方式:
需要設定tableViewrowHeight屬性,這裡設定為自動高度,告訴系統Cell的高度是不固定的,需要系統幫我們進行計算。然後設定tableViewestimatedRowHeight屬性,設定一個估計的高度。(我這裡用的代理方法,實際上都一樣)

原理:
這樣的話,在tableView被建立之後,系統會根據estimatedRowHeight屬性設定的值,為tableView設定一個估計的值。然後在Cell顯示的時候再獲取Cell的高度,並重新整理tableViewcontentSize

實現程式碼:
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自動佈局的方案,並且會講一下自動佈局的技巧,只要掌握技巧,佈局其實很簡單。

佈局小技巧:
UIScrollView新增的約束是定義其frame,設定contentSize是定義其內部大小。UIScrollView進行addSubview操作,都是將其子檢視新增到contentView上。
所以,新增到UIScrollView上的子檢視,對UIScrollView新增的約束都是作用於contentView上的。只需要按照這樣的思路給UIScrollView設定約束,就可以掌握設定約束的技巧了。

提前設定contentSize

// 提前設定好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

上面的例子是提前設定好UIScrollViewcontentSize的內部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);
}];

相關文章