NSLayoutConstraint、Mansonry 對比
NSLayoutConstraint 新增 AutoLayout 約束
首先,給一個 view 新增 AutoLayout 的原生方法:
- 使用 NSLayoutAttribute 建立一個 NSLayoutConstraint 物件;
- 然後 [view addConstraint:]
UIView *superView = self.view;
UIView *view1 = [UIView new];
[superView addSubView:view1];
// 禁用自動約束
view1.translatesAutoresizingMaskIntoConstraints = NO;
// 建立一個 view1 相對於 superView 的 left 約束
NSLayoutConstraint *leftConstraint
= [NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superView
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:10];
// 省略 30 行
NSLayoutConstraint *rightConstraint;
NSLayoutConstraint *topConstraint;
NSLayoutConstraint *bottomConstraint;
[view1 addConstraint:leftConstraint];
[view1 addConstraint:rightConstraint];
// 省略...
複製程式碼
使用 Masonry 新增 AutoLayout 約束
上文新增的約束,使用 Masonry 如下,簡化了大概 35 行程式碼:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superView).offset(10);
}];
複製程式碼
Masonry 新增約束流程解析
mas_makeConstraints 內部邏輯展開如下:
[view1 mas_makeConstraints:^(...) {
view1.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *maker = [[MASConstraintMaker alloc] initWithView:view1];
// 呼叫 block():
maker.left.equalTo(0);
maker.width.and.top.equalTo().offset(2);
[maker install];
}]
複製程式碼
可以從上述例子中看到,Masonry 新增約束呼叫的 [view1 mas_makeConstraints:] 裡,主要做了以下事情:
- 禁用 translatesAutoresizingMaskIntoConstraints 自動約束;
- 初始化一個 ConstraintMaker (約束工廠類),並給 maker 新增 上下左右/寬高 的約束(注意這一步只是將約束記錄到 maker 的 constraint array 中);
- 在 maker install 時,將 constraint array 中的約束逐個 add 到 view 上;
1:生成約束(並 record)
1.1 make.left
// 語法分析:
MASConstraintMaker *make = [MASConstraintMaker new];
// make.left 只是 get 方法( MASConstraintMaker 的 MASConstraint* left 屬性)
MASConstraint *mas = make.left;
複製程式碼
- 可以看到 make.left 是呼叫 .left 屬性;
- 但由於重寫了 left 屬性的 get 方法,所以,make.left 會呼叫 [make addConstraint:] 新增一條 NSLayoutAttributeLeft 型別的約束;
- left 屬性的 get 方法,最終 return 了一個 MASConstraint 物件,所以後續才能鏈式呼叫;
// MASConstraintMaker.m
- (MASConstraint *)left {
return [self constraint:nil addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
// 用 view 和 NSLayoutAttribute 構建 MASViewAttribute
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
// 用 MASViewAttribute 構建 MASViewConstraint
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
// ... 省略
if (!constraint) {
// 作用??
newConstraint.delegate = self;
// 將 MASViewConstraint 新增到 make.constraints(Array) 中,等 install 時再將約束 add 到 view 上
[self.constraints addObject:newConstraint];
}
// return 的 MASViewConstraint 可以繼續 鏈式呼叫
return newConstraint;
}
複製程式碼
1.2 make.left.and
MASConstraint *mas = make.left;
中可以看到 make.left
返回的是一個 MASConstraint *
型別。
make.left.and.with
and 和 with 作為 get 方法被呼叫,裡邊只是簡單的 return 了 MASConstraint self,所以可以繼續鏈式呼叫
1.3 make.left.and.width
MASConstraint *mas = make.left;
中可以看到 make.left
返回的是一個 MASConstraint *
型別。因此,鏈式呼叫到 .width 時,呼叫的不再是 maker 的方法,而是 MASConstraint
的方法:
// MASConstraint.m
- (MASConstraint *)width {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
}
複製程式碼
MASConstraint
是一個 Abstract 抽象類:只提供介面,內部是空實現,需要子類處理。因此 width 裡呼叫的 addConstraintWithLayoutAttribute:
會呼叫到子類 MASViewConstraint
中:
// MASViewConstraint.m
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
複製程式碼
MASViewConstraint
中並沒有生成或新增約束,只是呼叫 delegate 去處理。這個 delegate
剛好就是前邊的 maker
物件,所以又回到了上一步 make.left
一樣的邏輯(注意上一步中省略掉沒貼的程式碼):
// MASConstraintMaker.m
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
// 同 make.left ,省略...
// MASViewAttribute *viewAttribute = ;
// MASViewConstraint *newConstraint = ;
// make.left 時,此方法傳入的 constraint 是 nil,因此分析 make.left 時,這塊程式碼省略了
// make.left.width 到 .width 時,這裡傳入的 constraint 就是 make.left 生成的 newConstraint 物件
if ([constraint isKindOfClass:MASViewConstraint.class]) {
// 注: 此處的 constraint 即 left constraint;newConstraint 即 width constraint
NSArray *children = @[constraint, newConstraint];
// MASCompositeConstraint 是 MASConstraint 的子類,constraint group
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
// 替換約束:
// 替換已經記錄到 maker.constraintArray 中的 left constraint 約束為 constraint group
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
}
// ... 省略
// if 條件成立,已經 return,後續不會再重複 add constraint 到 array
}
// array 替換元素
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
NSUInteger index = [self.constraints indexOfObject:constraint];
[self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
複製程式碼
1.4 make.left.and.width.equalTo()
用於 make.left 作為 get 方法返回的是 MASViewConstraint
物件,可以繼續鏈式呼叫:make.left.equalTo(superView)
,即呼叫 MASViewConstraint.equalTo();
equalTo
入參是 id 型別的 attribute,attribute
型別可以是 MASViewAttribute、UIView、NSValue
,equalTo 方法內部對型別做了 if 判斷;
equalTo 是個巨集定義,展開後的呼叫鏈是:
qualTo(x) -> mas_equalTo(x) -> MASViewConstraint.equalToWithRelation(x, NSLayoutRelationEqual)
注意:
- 這裡呼叫過程中將 NSLayoutRelation 引數也傳入到了 MASViewConstraint.equalToWithRelation();
- equalToWithRelation 中儲存了 equalTo 傳入的
id 型別 attribute
和NSLayoutRelation
到 MASViewConstraint 物件中,並返回 MASViewConstraint 物件,以繼續鏈式呼叫;
// MASViewConstraint.m
- (MASConstraint * (^)(id))mas_equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
// ... 簡化
// 儲存了 equalTo 傳入的 attribute 和 NSLayoutRelation 型別
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
// 繼續返回 MASConstraint,鏈式呼叫
return self;
};
}
複製程式碼
1.5 make.left.equalTo().offset()
繼續鏈式呼叫 offset:make.left.equalTo(superView).offset(2);
注:
offset
和equalTo
不同,不是像equalTo
有入參的,所以可以加括號 equalTo(xxx);offset
方法是一個無入參方法,但是方法的返回值是一個有入參有返回值的 block 型別,因此可以 .offset(3) 呼叫;- 並且,由於 block 的返回值仍是 MASConstraint 物件,所以可以繼續鏈式呼叫;
簡化邏輯如下:
// make.left 是 get 方法
MASConstraint* mas = make.left;
MASConstraint*(^block)(CGFloat f) = mas.offset;
// 由於 .offset 的 返回值是 block,所以,可以直接呼叫:
mas.offset(2);
// mas.offset(2) 等同於:
block(2);
// block 的返回值
MASConstraint *mas2 = mas.offset(2);
複製程式碼
1.6 make.xxx
- 其它
make.center
make.insets.and.size
都是一樣的用法; make.left.priority(MASLayoutPriorityDefaultLow)
等於make.left.priorityLow
;
2:新增約束
2.1 [make install]
對 MASConstraintMaker *make 新增約束後,make install 最後執行約束:
// MASConstraintMaker.m
- (NSArray *)install {
// mas_remakeConstraints 的邏輯:先對已加的約束全部移除,再重新新增
if (self.removeExisting) {
NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
for (MASConstraint *constraint in installedConstraints) {
[constraint uninstall];
}
}
NSArray *constraints = self.constraints.copy;
// 遍歷 make 裡新增的每個 MASConstraint 並 install
for (MASConstraint *constraint in constraints) {
// mas_updateConstraints 的邏輯: 標記是否需要更新,稍後在 MASViewConstraint install 裡更新已加的約束
constraint.updateExisting = self.updateExisting;
// 最終 install 約束的還是在 MASViewConstraint 裡
[constraint install];
}
[self.constraints removeAllObjects];
return constraints;
}
複製程式碼
2.2 [constraint install]
// MASViewConstraint.m
- (void)install {
// ... 省略:避免重複新增的邏輯
// 取出 MASViewConstraint 記錄的 兩個 MASViewAttribute 物件
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
// ... 省略
// 1、構建一個 NSLayoutAttribute
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
// ...
// 建立完約束物件後,尋找約束該新增到那個View上:
if (self.secondViewAttribute.view) { // 如果是兩個檢視相對約束,就獲取兩種的公共父檢視。
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) { // 如果新增的是Width或者Height,那麼就新增到當前檢視上
self.installedView = self.firstViewAttribute.view;
} else { // 如果既沒有指定相對檢視,也不是Size型別的約束,那麼就將該約束物件新增到當前檢視的父檢視上
self.installedView = self.firstViewAttribute.view.superview;
}
// 是否已經新增了約束
MASLayoutConstraint *existingConstraint = nil;
// mas_updateConstraints 的邏輯
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) { // 存在,直接重新賦值
existingConstraint.constant = layoutConstraint.constant;
// ...
} else {
// 不存在,新增
// 2、[view addConstraint:NSLayoutAttribute]; !!!
[self.installedView addConstraint:layoutConstraint];
// ...
}
}
複製程式碼
程式碼技巧
block 靈活使用
block 寫法
@property (nonatomic, strong) UIView *(^myBlock)(NSLayoutAttribute attr);
self.myBlock = ^UIView *(NSLayoutAttribute attr) {
return greenView;
};
// blcok 在右側時,^ 在最前
dispatch_block_t t = ^void(void) {
};
UIView *(^block)(NSLayoutAttribute attr) = self.myBlock;
self.myBlock = block;
複製程式碼
更多 block 語法,見:How Do I Declare A Block in Objective-C?
block 作為入參簡化外部呼叫
在方法需要傳入一個 ConfigModel 時,使用 block 作為入參:讓外部無需關注 ConfigModel 初始化方式,只需要關注配置項
// 使用:
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(lastView).offset(2);
}];
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
// new 一個 maker,然後呼叫 block 傳出去
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
// 外部對 maker 配置後,maker install
block(constraintMaker);
return [constraintMaker install];
}
複製程式碼
應用案例: SDK 初始化
[SHWAccountSDK setupConfig:^(SHWAccountConfig *config) {
config.appKey = @"1";
config.secret = @"2";
}];
複製程式碼
block 入參作為 result 回撥
[SHWAccountSDK getSMSCode:phoneNum success:^(BOOL isSuccess) {
// getSMSCode 內部 new 了一個 Request 物件;
} failure:^(NSString *errCode, NSString *errMsg) {
}];
MyRequest *request;
[request startWithSuccess:^(BOOL isSuccess) {
NSLog(@"getSMSCodeAsyncWithPhoneNum success");
} failure:^(NSString *errCode, NSString *errMsg) {
NSLog(@"getSMSCodeAsyncWithPhoneNum fail");
}];
複製程式碼
block 作為返回值,鏈式呼叫
通常函式的入參是 block 用的比較多,但函式的返回值是 block 時,可以寫出鏈式呼叫的優雅寫法:
- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}
// 使用:
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_bottom).offset(padding);
}];
複製程式碼
原理:
MASConstraint *mas = [MASConstraint new];
MASConstraint*(^block)(CGFloat f) = mas.offset;
// 以下相等
block(2);
mas.offset(2);
// 由於 mas.offset 的返回值是一個 (入參是 CGFloat 的)block,在後邊直接追加 (2),相當於呼叫 block
// 並且,由於 block 的返回值仍是 mas 型別,所以後邊可以繼續鏈式呼叫
複製程式碼
對比:
// 返回值不是 block,只是 self class 時
- (MASConstraint *)setOffset:(CGFloat)offset {
self.offset = offset;
return self;
}
// 使用:
MASConstraint *mas = [MASConstraint new];
// 只能這樣呼叫
[[mas setOffset:3] setOffset:1];
複製程式碼
MASBoxValue: mas_equalTo、equalTo
mas_equalTo()
可以傳入多種型別入參:·
[view1 makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(view2);
make.left.mas_equalTo(3);
make.left.mas_equalTo(@(2));
make.left.mas_equalTo(view2.mas_left);
make.size.mas_equalTo(CGSizeMake(10, 10));
make.edges.equalTo(UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f));
make.height.equalTo(@[redView, blueView])
}];
複製程式碼
其定義如下:
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return ...;
};
}
// 儘管傳入的是 id 型別,但是解析也還是要支援變參和,並將 float,double,int 這樣的值型別資料轉換成和 equalTo 一樣的物件 NSNumber 資料:
static inline id _MASBoxValue(const char *type, ...) {
va_list v;
va_start(v, type);
id obj = nil;
if (strcmp(type, @encode(id)) == 0) {
id actual = va_arg(v, id);
obj = actual;
} else if (strcmp(type, @encode(CGPoint)) == 0) {
CGPoint actual = (CGPoint)va_arg(v, CGPoint);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(CGSize)) == 0) {
CGSize actual = (CGSize)va_arg(v, CGSize);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(int)) == 0) {
int actual = (int)va_arg(v, int);
obj = [NSNumber numberWithInt:actual];
} // ... 省略
va_end(v);
return obj;
}
複製程式碼
這也順便解釋了 mas_equalTo
和 equalTo
的區別:沒有區別,mas_equalTo
呼叫的還是 equalTo
,只是呼叫前對入參進行了 boxValue
轉換型別