iOS自動佈局——Masonry詳解
歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~
本文由鵝廠新鮮事兒發表於雲+社群專欄
作者:oceanlong | 騰訊 移動客戶端開發工程師
前言
UI佈局是整個前端體系裡不可或缺的一環。程式碼的佈局是設計語言與使用者視覺感受溝通的橋樑,不論它看起來多麼簡單或是瑣碎,但不得不承認,絕大部分軟體開發的問題,都是介面問題。那麼,如何高效的完成UI開發,也是軟體行業一直在克服的問題。

所以,軟體介面開發的核心點即是:如何減少UI設計稿的建模難度和減少建模轉化到程式碼的實現難度
最初iOS提供了平面直角座標系的方式,來解決佈局問題,即所謂的手動佈局。平面直角座標系確實是一套完備在理論,這在數學上已經驗證過了,只要我們的螢幕還是平面,它就肯定是有效的。但有效不一定高效,我們在日常的生活中,很少會用平面直角座標系來向人描述位置關係。更多的是依靠相對位置。
所幸,iOS為我們提供自動佈局的方法,來解決這一困境。

自動佈局的基本理念
其實說到本質,它和手動佈局是一樣的。對一個控制元件放在哪裡,我們依然只關心它的 (x, y, width, height)
。但手動佈局的方式是,一次性計算出這四個值,然後設定進去,完成佈局。但當父控制元件或螢幕發生變化時,子控制元件的計算就要重新來過,非常麻煩。
因此,在自動佈局中,我們不再關心 (x, y, width, height)
的具體值,我們只關心 (x, y, width, height)
四個量對應的約束。
約束
那麼何為約束呢?
obj1.property1 =(obj2.property2 * multiplier)+ constant value
子控制元件的某一個量一定與另一個控制元件的某一個量呈線性關係 ,這就是約束。
那麼,給 (x, y, width, height)
四個量,分別給一個約束,就可以確定一個控制元件的最終位置。
//建立左邊約束 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc];
這一段程式碼即是: 控制元件(blueView)的 x = rootView的x * 1.0 + 20 這裡一定要注意,這樣的一條約束,涉及了子控制元件和父控制元件,所以這條約束一定要新增到父控制元件中。
新增約束的規則:
- 如果兩個控制元件是父子控制元件,則新增到父控制元件中。
- 如果兩個控制元件不是父子控制元件,則新增到層級最近的共同父控制元件中。
示例
//關閉Autoresizing blueView.translatesAutoresizingMaskIntoConstraints = NO; //建立左邊約束 NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc]; //建立右邊約束 NSLayoutConstraint *rightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant:-20]; [self.view addConstraint:rightLc]; //建立底部約束 NSLayoutConstraint *bottomLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-20]; [self.view addConstraint:bottomLc]; //建立高度約束 NSLayoutConstraint *heightLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:50]; [blueView addConstraint: heightLc];

我們注意到,自動佈局其實工作分兩步:
- 建立檢視的約束
- 將約束新增到合適的位置約束關係從上面的描述中,已經非常清晰了。那麼如何尋找約束新增的合適位置呢?

到這裡,我們只是解決了 如何減少UI設計稿的建模難度的問題 ,顯然, 減少建模轉化到程式碼的實現難度 這個效果沒能達成。關於如何解決 減少建模轉化到程式碼的實現難度 的問題,
開源庫
上面的程式碼,我們可以看到,雖然自動佈局已經比手動佈局優雅不少了,但它依然行數較多。每條約束大約都需要三行程式碼,面對複雜的頁面,這樣開發出來,會很難閱讀。
Masonry 則為我們解決了這個問題。
ofollow,noindex">Masonry地址
引入Masonry
我們選擇使用 Cocoapods 的方式。引入比較簡單:
- 我們先在工程目錄下,建立 Podfile 檔案:

2.編輯 Podfile

其中,'IosOcDemo'就是我們工程的名字,根據需要,我們自行替換。
3.新增依賴
完成後,執行指令 pod install
。CocoaPods就會為我們自動下載並新增依賴。
實踐

這樣的一個程式碼,用手動佈局,我們大致的程式碼應該是這樣:
-(void)initBottomView { self.bottomBarView = [[UIView alloc]initWithFrame:CGRectZero]; self.bottomButtons = [[NSMutableArray alloc]init]; _bottomBarView.backgroundColor = [UIColor yellowColor]; [self addSubview:_bottomBarView]; for(int i = 0 ; i < 3 ; i++) { UIButton *button = [[UIButton alloc]initWithFrame:CGRectZero]; button.backgroundColor = [UIColor redColor]; [_bottomButtons addObject:button]; [self addSubview:button]; } } -(void)layoutBottomView { _bottomBarView.frame = CGRectMake(20, _viewHeight - 200, _viewWidth - 40, 200); for (int i = 0 ; i < 3; i++) { UIButton *button = _bottomButtons[i]; CGFloat x = i * (_viewWidth - 40 - 20 * 4) / 3 + 20*(i+1) + 20; CGFloat y = _viewHeight - 200; CGFloat width = (_viewWidth - 40 - 20 * 4) / 3; CGFloat height = 200; button.frame = CGRectMake(x, y, width, height); } }
我們來看一下,在 Masonry 的幫助下,我們可以把剛剛的程式碼寫成什麼樣的:
-(void)initBottomView { _bottomBarView = [[UIView alloc]initWithFrame:CGRectZero]; _bottomBarView.backgroundColor = [UIColor yellowColor]; _bottomBarView.translatesAutoresizingMaskIntoConstraints = NO; [self addSubview:_bottomBarView]; [_bottomBarView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self); }]; _bottomButtons = [[NSMutableArray alloc]init]; for(int i = 0 ; i < 3 ; i++) { UIButton *button = [[UIButton alloc]initWithFrame: CGRectZero]; button.backgroundColor = [UIColor redColor]; button.translatesAutoresizingMaskIntoConstraints = NO; [_bottomButtons addObject:button]; [_bottomBarView addSubview:button]; [button mas_makeConstraints:^(MASConstraintMaker *make) { if (i == 0) { make.left.mas_equalTo(20); }else{ UIButton *previousButton = _bottomButtons[i-1]; make.left.equalTo(previousButton.mas_right).with.offset(20); } make.top.mas_equalTo(_bottomBarView.mas_top); make.width.equalTo(_bottomBarView.mas_width).with.multipliedBy(1.0f/3).offset(-20*4/3); make.height.equalTo(_bottomBarView.mas_height); }]; } }
我們可以看到在 Masonry 的封裝下,程式碼變得非常簡練易讀,需要行數略有增加,但是計算過程減少了,我們能更加關注於多個UIView間的位置關係,這與當前的UI設計語言是契合的。所以 Masonry 能否讓我們更直觀地表達UI。
原始碼解讀
Masonry 的封裝很有魅力,那麼,我們可以簡單地來看一下,它是如何封裝的。我們再仔細看一下 Masonry 的API會發現,我們是直接在UIView上進行呼叫的。也就是說, Masonry 對UIView進行了擴充套件。
在 View+MASUtilities.h 中:
#if TARGET_OS_IPHONE || TARGET_OS_TV #import <UIKit/UIKit.h> #define MAS_VIEW UIView #define MAS_VIEW_CONTROLLER UIViewController #define MASEdgeInsets UIEdgeInsets
然後在 View+MASAdditions.h 中,我們看到了 Masonry 的擴充套件:
#import "MASUtilities.h" #import "MASConstraintMaker.h" #import "MASViewAttribute.h" /** *Provides constraint maker block *and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs */ @interface MAS_VIEW (MASAdditions) /** *following properties return a new MASViewAttribute with current view and appropriate NSLayoutAttribute */ @property (nonatomic, strong, readonly) MASViewAttribute *mas_left; @property (nonatomic, strong, readonly) MASViewAttribute *mas_top; @property (nonatomic, strong, readonly) MASViewAttribute *mas_right; @property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom; @property (nonatomic, strong, readonly) MASViewAttribute *mas_leading; @property (nonatomic, strong, readonly) MASViewAttribute *mas_trailing; @property (nonatomic, strong, readonly) MASViewAttribute *mas_width; @property (nonatomic, strong, readonly) MASViewAttribute *mas_height; @property (nonatomic, strong, readonly) MASViewAttribute *mas_centerX; @property (nonatomic, strong, readonly) MASViewAttribute *mas_centerY; @property (nonatomic, strong, readonly) MASViewAttribute *mas_baseline; @property (nonatomic, strong, readonly) MASViewAttribute *(^mas_attribute)(NSLayoutAttribute attr); ... /** *Creates a MASConstraintMaker with the callee view. *Any constraints defined are added to the view or the appropriate superview once the block has finished executing * *@param block scope within which you can build up the constraints which you wish to apply to the view. * *@return Array of created MASConstraints */ - (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block;
一些,適配的程式碼,我省略了,先看核心程式碼。在剛剛的例子中,我們正是呼叫的 mas_makeConstraints
方法。
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }
mas_makeConstraints
方法比較簡單,只是封裝了 MASConstraintMaker
初始化,設定約束和安裝。這裡的block就是我們剛剛在外層設定的約束的函式指標。也就是這一串:
^(MASConstraintMaker *make) { make.left.equalTo(self.view).with.offset(10); make.right.equalTo(self.view).with.offset(-10); make.height.mas_equalTo(50); make.bottom.equalTo(self.view).with.offset(-10); }
由於約束條件的設定比較複雜,我們先來看看初始化和安裝。
初始化
- (id)initWithView:(MAS_VIEW *)view { self = [super init]; if (!self) return nil; self.view = view; self.constraints = NSMutableArray.new; return self; }
初始化的程式碼比較簡單,將傳入的 view 放入 MASConstraintMaker
成員,然後建立 MASConstraintMaker
的約束容器(NSMutableArray)。
安裝
- (NSArray *)install { if (self.removeExisting) { NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view]; for (MASConstraint *constraint in installedConstraints) { [constraint uninstall]; } } NSArray *constraints = self.constraints.copy; for (MASConstraint *constraint in constraints) { constraint.updateExisting = self.updateExisting; [constraint install]; } [self.constraints removeAllObjects]; return constraints; }
安裝的程式碼分為三塊:
- 判斷是否需要移除已有的約束。如果需要,會遍歷已有約束,然後逐個
uninstall
-
copy
已有的約束,遍歷,並逐一install
-
remove
掉所有約束,並將已新增的constraints
返回。
install
的方法,還是繼續封裝到了 Constraint
中,我們繼續跟進閱讀:
我們會發現 Constraint
只是一個介面, Masonry 中對於 Constraint
介面有兩個實現,分別是: MASViewConstraint
和 MASCompositeConstraint
。這兩個類,分別是單個約束和約束集合。在上面的例子中,我們只是對單個 UIView
進行約束,所以我們先看 MASViewConstraint
的程式碼。以下程式碼 MASViewConstraint
進行了一定程度的簡化,省略了一些擴充套件屬性,只展示我們的例子中,會執行的程式碼:
- (void)install { if (self.hasBeenInstalled) { return; } ... MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item; NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute; MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item; NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute; // alignment attributes must have a secondViewAttribute // therefore we assume that is refering to superview // eg make.left.equalTo(@10) if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) { secondLayoutItem = self.firstViewAttribute.view.superview; secondLayoutAttribute = firstLayoutAttribute; } MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant]; layoutConstraint.priority = self.layoutPriority; layoutConstraint.mas_key = self.mas_key; if (self.secondViewAttribute.view) { MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; NSAssert(closestCommonSuperview, @"couldn't find a common superview for %@ and %@", self.firstViewAttribute.view, self.secondViewAttribute.view); self.installedView = closestCommonSuperview; } else if (self.firstViewAttribute.isSizeAttribute) { self.installedView = self.firstViewAttribute.view; } else { self.installedView = self.firstViewAttribute.view.superview; } MASLayoutConstraint *existingConstraint = nil; ... else { [self.installedView addConstraint:layoutConstraint]; self.layoutConstraint = layoutConstraint; [firstLayoutItem.mas_installedConstraints addObject:self]; } }
自動佈局是一種相對佈局,所以,絕大部分情況下,需要兩個 UIView
(約束方與參照方)。在上面的方法中:
-
firstLayoutItem
是約束方,secondLayoutItem
是參照方 -
firstLayoutAttribute
是約束方的屬性,secondLayoutAttribute
是參照方的屬性。 -
MASLayoutConstraint
就是NSLayoutConstraint
的子類,只是添加了mas_key屬性。到這裡,我們就與系統提供的API對應上了。
NSLayoutConstraint *leftLc = [NSLayoutConstraint constraintWithItem:blueView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:20]; [self.view addConstraint:leftLc];
再看看我們之前用系統API完成的例子,是不是格外熟悉?
那麼接下來,我們就是要閱讀
make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self);
是如何變成 firstLayoutItem
, secondLayoutItem
, firstLayoutAttribute
, secondLayoutAttribute
和 layoutRelation
的。
約束條件的設定
回到前面的:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; }
我們接下來,就要看block的實現:
block
其實是一個函式指標。此處真正呼叫的方法是:
make.left.equalTo(self).with.offset(20); make.right.equalTo(self).with.offset(-20); make.height.mas_equalTo(200); make.bottom.equalTo(self);
我們挑選其中一個,來看看原始碼實現:
left
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute]; } - (MASConstraint *)left { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]; } - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute]; MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]; if ([constraint isKindOfClass:MASViewConstraint.class]) { //replace with composite constraint NSArray *children = @[constraint, newConstraint]; MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self; [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } if (!constraint) { newConstraint.delegate = self; [self.constraints addObject:newConstraint]; } return newConstraint; }
在對單個 view
新增約束時, constraint
為nil。我們直接生成了一個新約束 newConstraint
。它的 firstViewAttribute
就是我們傳入的 NSLayoutAttributeLeft
equalTo
- (MASConstraint * (^)(id))equalTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationEqual); }; } - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { return ^id(id attribute, NSLayoutRelation relation) { if ([attribute isKindOfClass:NSArray.class]) { NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation"); NSMutableArray *children = NSMutableArray.new; for (id attr in attribute) { MASViewConstraint *viewConstraint = [self copy]; viewConstraint.layoutRelation = relation; viewConstraint.secondViewAttribute = attr; [children addObject:viewConstraint]; } MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self.delegate; [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } else { NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation"); self.layoutRelation = relation; self.secondViewAttribute = attribute; return self; } }; }
此處,我們依然先看attribute不是 NSArray
的情況。這裡在單個屬性的約束中,就比較簡單了,將 relation
和 attribue
傳入 MASConstraint
對應的成員。
在上面介紹 install
方法時,我們就曾提到過:
MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant];
firstLayoutItem
和 secondLayoutItem
在 install
方法中已收集完成,此時,經過 left
和 equalTo
我們又收集到了: firstViewAttribute
、 secondViewAttribute
和 layoutRelation
勝利即在眼前。
- (MASConstraint * (^)(CGFloat))offset { return ^id(CGFloat offset){ self.offset = offset; return self; }; } - (void)setOffset:(CGFloat)offset { self.layoutConstant = offset; }
通過OC的set語法, Masonry
將 offset 傳入layoutConstant。
至此, layoutConstraint
就完成了全部的元素收集,可以使用新增約束的方式,只需要解決最後一個問題,約束新增到哪裡呢?我們似乎在呼叫時,並不需要關心這件事情,那說明框架幫我們完成了這個工作。
closestCommonSuperview
我們在MASViewConstraint中,可以找到這樣一段:
if (self.secondViewAttribute.view) { MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; NSAssert(closestCommonSuperview, @"couldn't find a common superview for %@ and %@", self.firstViewAttribute.view, self.secondViewAttribute.view); self.installedView = closestCommonSuperview; } else if (self.firstViewAttribute.isSizeAttribute) { self.installedView = self.firstViewAttribute.view; } else { self.installedView = self.firstViewAttribute.view.superview; }
注意到, closetCommonSuperview
就是Masonry為我們找到的最近公共父控制元件。
- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view { MAS_VIEW *closestCommonSuperview = nil; MAS_VIEW *secondViewSuperview = view; while (!closestCommonSuperview && secondViewSuperview) { MAS_VIEW *firstViewSuperview = self; while (!closestCommonSuperview && firstViewSuperview) { if (secondViewSuperview == firstViewSuperview) { closestCommonSuperview = secondViewSuperview; } firstViewSuperview = firstViewSuperview.superview; } secondViewSuperview = secondViewSuperview.superview; } return closestCommonSuperview; }
實現也比較簡單。
至此,我們完成了所有準備,就可以開始愉快的自動佈局啦。
以上就是 Masonry 對iOS自動佈局封裝的解讀。
如有問題,歡迎指正。
問答 iOS:如何使用自動佈局約束? 相關閱讀 走進 Masonry iOS自動佈局框架之Masonry iOS學習——佈局利器Masonry框架原始碼深度剖析 【每日課程推薦】機器學習實戰!快速入門線上廣告業務及CTR相應知識
此文已由作者授權騰訊雲+社群釋出,更多原文請點選
搜尋關注公眾號「雲加社群」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社群!