Masonry思路解析
当前iOS最流行的布局方式就是使用autoLayout,本文就时下最流行的Masonry进行分析,解剖其实现思路。
我们都知道UIKit自带的布局代码超级复杂,代码繁多不够简洁。
UIView *view = [[UIView alloc]init];
[self.view addSubview:view];
view.translatesAutoresizingMaskIntoConstraints = NO;
[[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
[[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
[[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
[[NSLayoutConstraint constraintWithItem:view attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:0] setActive:YES];
怎样才能实现简单的autoLayout实现方式呢?
Masonry引入的链式响应方式。如
// 通过一行或很少的代码即可完成autolayout,Masonry其实就是UIKit中NSLayoutConstraint的封装
view.top.left.right.equalTo(self.view.mas_top).offset(10)
咱们一步步实现自己的Masonry。
NSLayoutConstraint简单解析
由于是封装现有的UIKit的NSLayoutConstraint,所有我们要分析下UIKit自带的布局约束方法有什么特征。
/**
约束方法
@param view1 约束左边的view1
@param attr1 view1需要约束的属性
@param relation 关系描述
@param view2 约束有边的view2
@param attr2 view2的布局属性
@param multiplier 比例关系
@param c 偏移量
@return 返回约束模型
*/
+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;
一个正常的约束的关系公式为:view1.attr1 = view2.attr2 * multiplier + c
一、MAAutoLayout(实现"view1.attr1")
我们需要给UIView添加一个布局管理属性 ma_layout(MAAutoLayout类型)
,方便之后布局管理。
MAAutoLayout管理自己的属性,需要知道view1和attr1,所以MAAutoLayout可以设计为
@interface MAAutoLayout : NSObject
- (nonnull instancetype)initWithView:(UIView * _Nonnull)view;
// 基本操作,依据NSLayoutAttribute创建所有的方法,用户链式响应的第一步
@property (nonatomic, strong, readonly) MAAutoLayoutMaker * _Nonnull left;
@property (nonatomic, strong, readonly) MAAutoLayoutMaker * _Nonnull top;
#####省略 right, bottom等,详细请看GitHub上的demo #####
// 激活
- (void)active;
// 取消
- (void)deactivate;
@end
此时我们就可以使用view.ma_layout.top 来替代公式中的view1.top。
二、MAAutoLayoutMaker(实现"= id * multiplier + c")
.equalTo(self.view)需要在MAAutoLayoutMaker中创建有关关系(NSLayoutRelation)的属性。
@interface MAAutoLayoutMaker : NSObject
@property (nullable, nonatomic,strong, readonly) NSLayoutConstraint *layoutConstraint;
- (nonnull instancetype)initWithFirstItem:(nonnull id)firstItem firstAttribute:(NSLayoutAttribute)firstAttribute;
// 偏移量
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat offset))offset;
// 关系
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(id _Nonnull attr))equalTo;
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(id _Nonnull attr))greaterThanOrEqualTo;
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(id _Nonnull attr))lessThanOrEqualTo;
// 赋值
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat constant))kf_equal;
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat constant))kf_greaterThanOrEqual;
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat constant))kf_lessThanOrEqual;
// 倍数
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(CGFloat multiplier))multiplier;
// 权重
- (MAAutoLayoutMaker * _Nonnull (^_Nonnull)(UILayoutPriority priority))priority;
- (BOOL)isActive;
- (nonnull NSLayoutConstraint *)active;
- (void)deactivate;
@end
有了MAAutoLayoutMaker,我们就能轻易的实现view.top.equalTo().offset(10).multiplier(2).但是equalTo中的属性,也就是view2.attr2还没发表示。
三、MAViewAttribute(实现"view2.attr2")
view2.attr2需要在MAViewAttribute保存view2的NSLayoutAttribute。
@interface UIView (MAAutoLayout)
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_left;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_top;
#####省略 right, bottom等,详细请看GitHub上的demo #####
//iOS11 safeArea
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideTop;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideBottom;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideLeft;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaLayoutGuideRight;
@end
@interface UIViewController (MAAutoLayout)
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_topLayoutGuide;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_bottomLayoutGuide;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_topLayoutGuideTop;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_topLayoutGuideBottom;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_bottomLayoutGuideTop;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_bottomLayoutGuideBottom;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaTopLayoutGuide;
@property (nonatomic, strong, readonly) MAViewAttribute * _Nonnull ma_safeAreaBottomLayoutGuide;
@end
此时调用布局代码的方式为
view.kf_layout.top.equalTo(self.view.ma_top).offset(10).active;
view.kf_layout.left.equalTo(self.view.ma_left).offset(10).active;
view.kf_layout.bottom.equalTo(self.view.ma_bottom).offset(-10).active;
view.kf_layout.right.equalTo(self.view.ma_right).offset(-10).active;
四、UIView封装Block
按照上面的方式布局没有任何问题,但总显得不够优雅,可以将布局代码包装在一个block中,代码更集中也更便于管理。
// 在UIView的Category中添加下面的方法
- (void)ma_makeConstraints:(void(^_Nonnull)(MAAutoLayout * _Nonnull make))make;
- (void)ma_remakeConstraints:(void(^_Nonnull)(MAAutoLayout * _Nonnull make))make;
此时的调用方式就变为:
[view ma_makeConstraints:^(MAAutoLayout * _Nonnull make) {
make.top.equalTo(self.view.ma_top).offset(10).active;
make.left.equalTo(self.view.ma_left).offset(10).active;
make.bottom.equalTo(self.view.ma_bottom).offset(-10).active;
make.right.equalTo(self.view.ma_right).offset(-10).active;
}];
五、代码的实现
步骤四已经基本实现了链式布局的方式,但是每行都要调用一次active方法,总显得不够优雅。我们可以想办法在Block执行完成时,一次性激活所有约束,因此需要在MAAutoLayout中添加一个数组用于保存所有的约束。
MAAutoLayout.m的实现如下:
@interface MAAutoLayout()
// 用于保存约束的数组
@property (nonatomic,strong) NSMutableArray<MAAutoLayoutMaker *> *constraints;
@property (nonatomic,weak) id view;
@end
@implementation MAAutoLayout
// 初始化时,保存view1
- (id)initWithView:(UIView *)view{
self = [super init];
if (!self) return nil;
self.view = view;
// 使用autoLayout需要手动设置translatesAutoresizingMaskIntoConstraints为NO
view.translatesAutoresizingMaskIntoConstraints = NO;
self.constraints = [NSMutableArray array];
return self;
}
#pragma mark - standard Attributes
- (MAAutoLayoutMaker *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
####### 篇幅有限仅显示top的实现,left等相同的类比调用addConstraintWithLayoutAttribute实现#########
- (MAAutoLayoutMaker *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MAAutoLayoutMaker *maker = [[MAAutoLayoutMaker alloc] initWithFirstItem:self.view firstAttribute:layoutAttribute];
// 将这个约束添加到数组中
[self.constraints addObject:maker];
return maker;
}
// 激活所有的约束
- (void)active{
for (MAAutoLayoutMaker *maker in self.constraints) {
if (!maker.isActive) {
[maker active];
}
}
}
// 撤销所有的约束
- (void)deactivate{
[self.constraints makeObjectsPerformSelector:@selector(deactivate)];
[self.constraints removeAllObjects];
}
@end
MAAutoLayoutMaker需要保存所有的相关NSLayoutConstraint的属性,是MAAutoLayout的核心。
MAAutoLayoutMaker.m的实现如下:
@interface MAAutoLayoutMaker()
@property (nullable, nonatomic,weak) id firstItem;
@property (nonatomic, assign) NSLayoutAttribute firstAttribute;
@property (nullable, nonatomic,weak) id secondItem;
@property (nonatomic, assign) NSLayoutAttribute secondAttribute;
@property (nonatomic, assign) NSLayoutRelation relation;
@property (nonatomic, assign) CGFloat multiplierValue;
@property (nonatomic, assign) CGFloat constant;
@property (nonatomic, assign) UILayoutPriority priorityValue;
@property (nonatomic,strong) NSLayoutConstraint *layoutConstraint;
@end
@implementation MAAutoLayoutMaker
- (instancetype)initWithFirstItem:(id)firstItem firstAttribute:(NSLayoutAttribute)firstAttribute{
self = [super init];
if (!self) return nil;
self.firstItem = firstItem;
self.firstAttribute = firstAttribute;
self.secondItem = nil;
self.secondAttribute = NSLayoutAttributeNotAnAttribute;
self.multiplierValue = 1.0;
self.constant = 0;
self.priorityValue = UILayoutPriorityRequired;
return self;
}
- (MAAutoLayoutMaker *(^)(CGFloat))offset{
return ^id(CGFloat offset){
self.constant = offset;
return self;
};
}
- (MAAutoLayoutMaker * _Nonnull (^)(id _Nonnull))equalTo{
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MAAutoLayoutMaker * _Nonnull (^)(CGFloat))kf_equal{
return ^id(CGFloat constant) {
return self.equalToWithRelation(@(constant), NSLayoutRelationEqual);
};
}
- (MAAutoLayoutMaker * _Nonnull (^)(UILayoutPriority))priority{
return ^(UILayoutPriority priority) {
self.priorityValue = priority;
return self;
};
}
- (MAAutoLayoutMaker * _Nonnull (^)(CGFloat))multiplier{
return ^(CGFloat multiplier) {
self.multiplierValue = multiplier;
return self;
};
}
- (BOOL)isActive{
return self.layoutConstraint != nil;
}
// 核心方法,通过上面的便携方法给firstItem,firstAttribute,secondItem, secondAttribute, relation, multiplierValue, constant, priorityValue,在active方法里创建layoutConstraint并激活。
- (NSLayoutConstraint *)active{
if (self.layoutConstraint) self.layoutConstraint.active = NO;
if (self.firstItem) {
self.layoutConstraint = [NSLayoutConstraint constraintWithItem:self.firstItem attribute:self.firstAttribute relatedBy:self.relation toItem:self.secondItem attribute:self.secondAttribute multiplier:self.multiplierValue constant:self.constant];
self.layoutConstraint.priority = self.priorityValue;
self.layoutConstraint.active = YES;
}
return self.layoutConstraint;
}
- (void)deactivate{
[self.layoutConstraint setActive:NO];
self.layoutConstraint = nil;
}
#pragma mark private
- (MAAutoLayoutMaker * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
self.relation = relation;
if ([attribute isKindOfClass:[UIView class]]) {
self.secondItem = attribute;
self.secondAttribute = self.firstAttribute;
}else if ([attribute isKindOfClass:[MAViewAttribute class]]){
self.secondItem = ((MAViewAttribute *)attribute).item;
self.secondAttribute = ((MAViewAttribute *)attribute).layoutAttribute;
}else if ([attribute isKindOfClass:[NSNumber class]]){
self.secondItem = nil;
self.secondAttribute = NSLayoutAttributeNotAnAttribute;
self.constant = ((NSNumber *)attribute).floatValue;
}else{
NSAssert(attribute, @"格式不正确,必须是UIView或MAAutoLayoutMaker或NSNumber");
}
self.relation = relation;
return self;
};
}
@end
UIView+MAAutoLayout.m的实现如下:
@implementation UIView (MAAutoLayout)
static char kInstalledMAAutoLayoutKey;
- (MAAutoLayout *)ma_layout {
MAAutoLayout *autolayout = objc_getAssociatedObject(self, &kInstalledMAAutoLayoutKey);
if (!autolayout) {
autolayout = [[MAAutoLayout alloc] initWithView:self];
objc_setAssociatedObject(self, &kInstalledMAAutoLayoutKey, autolayout, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return autolayout;
}
- (void)ma_makeConstraints:(void (^)(MAAutoLayout *))make{
make(self.ma_layout);
[self.ma_layout active];
}
- (void)ma_remakeConstraints:(void (^)(MAAutoLayout * _Nonnull))make{
[self.ma_layout deactivate];
[self ma_makeConstraints:make];
}
- (MAViewAttribute *)ma_left{
return [self viewAttribute:NSLayoutAttributeLeft];
}
#####省略right等,详细请看GitHub上的demo#####
#pragma mark - iOS11 safeArea
- (MAViewAttribute *)ma_safeAreaLayoutGuideTop{
return [self safeAreaViewAttribute:NSLayoutAttributeTop];
}
#####省略safeAreaLeft等,详细请看GitHub上的demo#####
- (UIEdgeInsets)ma_safeAreaInsets{
UIEdgeInsets safeInsets = UIEdgeInsetsZero;
#ifdef __IPHONE_11_0
if (@available(iOS 11.0, *)) {
safeInsets = self.safeAreaInsets;
}
#endif
return safeInsets;
}
#pragma mark - private
- (MAViewAttribute *)viewAttribute:(NSLayoutAttribute)layoutAttribute {
return [[MAViewAttribute alloc] initWithItem:self layoutAttribute:layoutAttribute];
}
- (MAViewAttribute *)safeAreaViewAttribute:(NSLayoutAttribute)layoutAttribute {
id item = self;
#ifdef __IPHONE_11_0
if (@available(iOS 11.0, *)) {
item = self.safeAreaLayoutGuide;
}
#endif
return [[MAViewAttribute alloc] initWithItem:item layoutAttribute:layoutAttribute];
}
@end
UIViewController+MAAutoLayout.m的实现如下:
@implementation UIViewController (MAAutoLayout)
- (MAViewAttribute *)ma_topLayoutGuideTop{
return [[MAViewAttribute alloc] initWithItem:self.topLayoutGuide layoutAttribute:NSLayoutAttributeTop];
}
#####省略topLayoutGuideBottom等,详细请看GitHub上的demo#####
- (MAViewAttribute *)ma_safeAreaTopLayoutGuide{
id item = self.topLayoutGuide;
NSLayoutAttribute attribute = NSLayoutAttributeBottom;
#ifdef __IPHONE_11_0
if (@available(iOS 11.0, *)) {
item = self.view.safeAreaLayoutGuide;
attribute = NSLayoutAttributeTop;
}
#endif
return [[MAViewAttribute alloc] initWithItem:item layoutAttribute:attribute];
}
- (MAViewAttribute *)ma_safeAreaBottomLayoutGuide{
id item = self.bottomLayoutGuide;
NSLayoutAttribute attribute = NSLayoutAttributeTop;
#ifdef __IPHONE_11_0
if (@available(iOS 11.0, *)) {
item = self.view.safeAreaLayoutGuide;
attribute = NSLayoutAttributeBottom;
}
#endif
return [[MAViewAttribute alloc] initWithItem:item layoutAttribute:attribute];
}
@end
此时完成了AutoLayout的简单封装。这样封装还有一个不便的地方,就是每个view每次都需要四个约束才能完成布局。像Masonry就提供了很多遍历方法,比如 make.top.left.right.bottom.equalTo(self.view).offset(10);或make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(10, 10, 10, 10));这是一条语句添加了多个约束,所以Masonry添加了MASCompositeConstraint的概念。
MAAutoLayout通过一个文件已经实现了链式响应的处理,如果实现的autoLayout比较简单,引入Masonry这么大的库有些浪费,可以使用MAAutoLayout,简单使用也挺好。
以上只是我的理解,有错误的地方请大家指出,大家多沟通交流。
附上demo地址