歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~javascript
做者:oceanlong | 騰訊 移動客戶端開發工程師java
UI佈局是整個前端體系裏不可或缺的一環。代碼的佈局是設計語言與用戶視覺感覺溝通的橋樑,不論它看起來多麼簡單或是瑣碎,但不得不認可,絕大部分軟件開發的問題,都是界面問題。那麼,如何高效的完成UI開發,也是軟件行業一直在克服的問題。git
因此,軟件界面開發的核心點便是:如何減小UI設計稿的建模難度和減小建模轉化到代碼的實現難度github
最初iOS提供了平面直角座標系的方式,來解決佈局問題,即所謂的手動佈局。平面直角座標系確實是一套完備在理論,這在數學上已經驗證過了,只要咱們的屏幕仍是平面,它就確定是有效的。但有效不必定高效,咱們在平常的生活中,不多會用平面直角座標系來向人描述位置關係。更多的是依靠相對位置。bash
所幸,iOS爲咱們提供自動佈局的方法,來解決這一困境。app
其實說到本質,它和手動佈局是同樣的。對一個控件放在哪裏,咱們依然只關心它的(x, y, width, height)
。但手動佈局的方式是,一次性計算出這四個值,而後設置進去,完成佈局。但當父控件或屏幕發生變化時,子控件的計算就要從新來過,很是麻煩。框架
所以,在自動佈局中,咱們再也不關心(x, y, width, height)
的具體值,咱們只關心(x, y, width, height)
四個量對應的約束。機器學習
那麼何爲約束呢?ide
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則爲咱們解決了這個問題。
咱們選擇使用Cocoapods的方式。引入比較簡單:
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);
複製代碼
咱們挑選其中一個,來看看源碼實現:
- (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
- (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
就完成了所有的元素收集,可使用添加約束的方式,只須要解決最後一個問題,約束添加到哪裏呢?咱們彷佛在調用時,並不須要關心這件事情,那說明框架幫咱們完成了這個工做。
咱們在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自動佈局封裝的解讀。
若有問題,歡迎指正。
問答
相關閱讀
此文已由做者受權騰訊雲+社區發佈,更多原文請點擊
搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!