在了解一个开源库之前,我们需要先搞清楚一件事情:
这个开源库解决了什么问题?
熟悉的人都知道:
Masonry 将 NSLayoutConstraint 进行了封装,使用了优雅高效易读的链式语法,让 Objective-C 开发者在手写 Autolayout 的时候不再那么麻烦。
因此,在看本文之前,你需要知道,什么是 Autolayout?
如果你已经知道什么是 Autolayout 了,那你至少还应该知道,如何使用代码来对视图添加约束?
现在应该知道,使用代码来写 Autolayout,苹果官方的 API 使用繁琐,代码量大,不易阅读。Masonry 非常完美的解决了这个问题。
Masonry 的 Github 地址:https://github.com/Masonry/Masonry
如果英语能力还可以的话,看它的 Github 里的 Readme 就能看明白很多东西。
这里有一个如何使用 Masonry 的教程写的不错:http://adad184.com/2014/09/28/use-masonry-to-quick-solve-autolayout/
我们先看看 NSLayoutConstraint 的创建方法:
// UIKit
// NSLayoutConstraint.h
/* Create constraints explicitly. Constraints are of the form "view1.attr1 = view2.attr2 * multiplier + constant"
If your equation does not have a second view and attribute, use nil and NSLayoutAttributeNotAnAttribute.
*/
+(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 + constant
再来看它的使用:
// NSLayoutConstraint
[superView addConstraint:[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:superView
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:10]];
// 根据上面的定义,这段代码的结果是:
// view1.top = superView.top * 1.0 + 10;
// 再看看 Masonry 的做法
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(10);
}];
// 简直完美!
Masonry 非常完美的解决了系统提供的复杂使用方式,让写出来的代码简洁,又易懂。
可以思考一下,如果我们要做这样一个封装,该怎么做?
思路:
我们首先需要一个类,就像 NSLayoutConstraint 一样,可以管理所有的东西,然后用简洁的输入方式输入所需要的内容,然后再调用一个方法,把需要的参数传递给 NSLayoutConstraint,并且生成,然后添加到 UIView 上。
最好是可以做到这样:
// 这将是我们的最终目标,请牢记这个,后面我们所有的想法,都是为了这个目标而努力。
view1.top = superView.top * 1.0 + 10;
所以我们需要的属性有上面的例子中所需要的所有东西:view1 , top , = , superView , top , 1.0 , 10. 根据 NSLayoutConstraint 的创建方法:
View1 和 superView 是 UIView 类型,
top 是 NSLayoutAttribute,
= 是 NSLayoutRelation,
1.0 是乘积, CGFloat 类型,
10 是偏移量 CGFloat 类型,
然后我们可以调用一个方法来完成这个约束的安装。
可是问题来了:
这跟系统提供的方法有什么区别呢?
Masonry 是如何实现链式语法的?
Masonry 主要的类结构有:
MASConstraint:继承自 NSObject,是我们上一部分设想的那个类,负责收集 NSLayoutConstraint 的创建方法需要的参数,并给对应的 View 添加 Constraint,不过这个类只是基础的抽象类,只负责收集信息并且提供一些基础方法和属性。
MASViewConstraint:继承自 MASConstraint,使用 MASViewAttribute 保存 view1 和 superView 与其 NSLayoutAttribute,然后通过一个方法来给 view1 添加约束。
MASViewAttribute:继承自 NSObject,负责将 view 和 NSLayoutAttribute 绑定在一起,目的是简化代码,不用在 MASViewConstraint 里面自己维护 view 和 NSLayoutAttribute 的关系。
MASCompositeConstraint:继承自 MASConstraint,批量存储 MASConstraint/MASViewConstraint,并提供批量添加约束的方法。
MASLayoutConstraint:继承自 NSLayoutConstraint,使用系统方法用来做最后的 constraint 生成,多了一个 key 的属性,来做 Debug。
MASConstraintMaker:继承自 NSObject,最重要的一个类,每个 MASConstraintMaker 维护一个 View,用来处理这个 View 相关的约束,这个 view 每一个相关的约束都是一个 MASViewConstraint,有一个数组 Constraints 来维护这些数据。最后通过一个方法,给 view 批量添加约束。
View+MASAdditions:因为我们的目标是做到可以直接使用 view.top… 所以我们必须给 UIView 加一个分类来处理这些,其实相关的内容都只是操作 MASConstraintMaker。
这个类是我们设想的最基础的类,按道理它应该负责保存 NSLayoutConstraint 生成方法所需要的所有参数。
但是 Masonry 没有这么做,Masonry 只把对 view 的 layoutAttribute 使用属性保存了起来:
NSLayoutConstraint 中的两个枚举类型:NSLayoutAttribute、 NSLayoutRelation
乘积 multiplier (CGFloat)
偏移量 constant (CGFloat)
优先级 UILayoutPriority (float)
并且生成了一些其他的衍生属性,比如 offset, insets, sizeOffset, centerOffset, dividedBy 等。
这个类是一个非常标准的抽象类,将基础的数据全部处理掉,然后在 MASConstraint+Private.h 中使用分类 MASConstraint (Abstract) 和一个匿名分类来声明几个基类和子类都将使用到的方法,并且在这里声明代理和代理方法。这一段很有意思,对于开发者来说应该能提供很多灵感。使用分类和协议,能帮你减少很多代码。
这里是链式语法的比较关键的一步,就是 view.top.equalTo(superView.top)
的 eauqlTo(),我们先看看它是如何定义的:
// MASConstraint.h
/**
* Sets the constraint relation to NSLayoutRelationEqual
* returns a block which accepts one of the following:
* MASViewAttribute, UIView, NSValue, NSArray
* see readme for more details.
*/
- (MASConstraint * (^)(id attr))equalTo;
看了就比较明白了,其实它就是将 block 作为返回值,而这个 block 的返回值就是 MASConstraint
自己,这样可以继续使用点语法操作其他内容,看看实现:
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
在这里,- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation
这个方法只是一个抽象的声明而已,因为在 MASConstraint 里面没有涉及到任何的 UIView,因此这些只是概念,具体的实现还是要到 MASViewConstraint 里面。
同理:
- (MASConstraint * (^)(CGFloat offset))offset;
- (MASConstraint * (^)(CGFloat multiplier))multipliedBy;
等等,也是如此的做法。
当基础类搞定,只能在抽象的意义上设定 view 的 top、left、right、bottom,并没有涉及到具体的 view。在这里就将来完成这一步。对于 NSLayoutConstraint 的生成方法,view 是有顺序的,而且在 NSLayoutConstraint 的属性中,也有 firstItem 、firstAttribute、secondItem、secondAttribute。因此我们也需要这样的东西。不过 Masonry 并没有这么做,它新创建了一个类,叫 MASViewAttribute ,负责将 view 和 attribute 绑定在一起。于是就有了如下属性:
第一个视图和它的约束 firstViewAttribute ( MASViewAttribute)
第二个视图和它的约束 secondViewAttribute ( MASViewAttribute)
然后再配一个如下的初始化方法:
// MASViewConstraint.h
/**
* initialises the MASViewConstraint with the first part of the equation
*
* @param firstViewAttribute view.mas_left, view.mas_width etc.
*
* @return a new view constraint
**/
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute;
还记得我们当初的设想么?
view1.top = superView.top * 1.0 + 10;
看看目前为止的代码执行是不是能满足这个要求了:
使用初始化方法,将 view1.top 整合成 MASViewAttribute 类型的实例,并赋值给 firstViewAttribute 属性
代码执行到 view1.top = 的时候,调用 equal 方法
代码执行到 view1.top = superView.top 的时候,将 superView.top 整合成 MASViewAttribute 类型的实例,并赋值给 secondViewAttribute 属性
代码执行到 view1.top = superView.top*1.0 的时候,将 1.0 保存到 multiplier 属性
代码执行到 view1.top = superView.top*1.0 + 10 的时候,将 10 保存到 constrant 属性
完全满足了!然后我们加一个 install (安装) 方法,来调用 NSLayoutConstraint 的创建方法就可以了。
这里最重要的实现,就是将 MASConstraint 里面定义的一些抽象方法具体实现,因为在这里已经有 view、superView 和对应的 NSLayoutConstraint。现在我们可以看看 MASConstraint 中 - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation
是怎么实现的了:
- (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.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;
}
};
}
if
里面的内容是批量添加的代码,目前来说不是重点。重点看 else
里面的内容,其实很简单,只是记录一下。 self.layoutRelation
就是 NSLayoutRelation
类型的,在 view1.top = superView.top * 1.0 + 10
里面负责的是 =
的部分,当然也可以是 >=
或者 <=
。
同理:
- (MASConstraint * (^)(CGFloat offset))offset;
- (MASConstraint * (^)(CGFloat multiplier))multipliedBy;
等等,也是如此的做法。
offset() 是控制偏移量的,对应的内容是 +10
,它最终的设置的是 self.layoutConstant。
multipliedBy() 是控制倍数的,对应的内容是 1.0
,它最终的设置的是 self.layoutMultiplier。
当然还有很多衍生用法,基本都是来控制这 self.layoutConstant
和 self.layoutMultiplier
这两个属性的。
这个类是用来绑定 view 和 NSLayoutAttribute 的,对于我们的设想,它达到的效果是 view1.top / superView.top, 它的属性有:
视图 view (UIView)
约束位置 layoutAttribute (NSLayoutAttribute)
有了以上这些,我们就可以设置一个视图的某一条约束了:
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:view1 layoutAttribute:NSLayoutAttributeTop];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
// 先忽略 superView.top 的问题,后面会讲
newConstraint.equalTo(superView.top).offset(10). multipliedBy(1.0);
这样已经方便很多了,但是依然需要创建 MASViewAttribute
和 MASViewConstraint
这两个东西,如果我们有一个类来维护这些东西,那就更方便了,于是就有了 MASConstraintMaker。
按照我们的想法,上面的几个类已经可以完成我们想要的了,但是问题是,这样依然不能做到简化语法的目的。
Masonry 最关键的地方在于这个类,它保存一个主视图 view1,然后针对这个主视图,使用一个 NSArray 来统一管理 MASViewConstraint。
这样,我们初始化一个 MASConstraintMaker 类,并赋值一个 view,然后添加其他约束就可以了。因此需要将 view 的基础约束添加成属性来调用:
// MASConstraintMaker.h
@property (nonatomic, strong, readonly) MASConstraint *left;
@property (nonatomic, strong, readonly) MASConstraint *top;
@property (nonatomic, strong, readonly) MASConstraint *right;
@property (nonatomic, strong, readonly) MASConstraint *bottom;
@property (nonatomic, strong, readonly) MASConstraint *leading;
@property (nonatomic, strong, readonly) MASConstraint *trailing;
@property (nonatomic, strong, readonly) MASConstraint *width;
@property (nonatomic, strong, readonly) MASConstraint *height;
@property (nonatomic, strong, readonly) MASConstraint *centerX;
@property (nonatomic, strong, readonly) MASConstraint *centerY;
@property (nonatomic, strong, readonly) MASConstraint *baseline;
// 这里的 MAS_VIEW 其实就是 UIView,Masonry 自己定义的宏
- (id)initWithView:(MAS_VIEW *)view;
这里我们使用 left 来举例,readonly 是因为这个约束不希望被外部修改,因为 Masonry 特有的链式语法,使用点语法 view.left
其实是为了利用 left 的 get 方法,然后创建一个 对应的 MASViewConstraint 并且存到数组中:
// MASConstraintMaker.m
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
// 创建 MASViewAttribute ,将 NSLayoutAttribute 和 view 绑定起来,
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;
}
这个想法是 Masonry 第二精妙之处。我们可以理解为,将 MASConstraintMaker 替换成 view。
MASConstraintMaker *make = [[MASConstraintMaker alloc] initWithView:view1];
make.left.equalTo(superView.mas_top).offset(10).multipliedBy(1.0);
以此类推,将 left、right、top、bottom 都添加好以后,调用一个方法,就可以将一个 View 的所有约束都安装好了。
现在看来,已经非常接近我们的目标 view1.top = superView.top * 1.0 + 10;
了,不过还是要创建一个 MASConstraintMaker
,我们也需要想办法简化。
上面的内容中,有一个关键的点,superView.top
是从哪里来的?UIView 并没有这样的属性。于是我们需要创建一个 UIView 的分类,给它添加几个属性:
// View+MASAdditions.h
// 为了不与其他库冲突,添加前缀是一个三方库最基本的操守
@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);
这样,我们就可以调用 view1.top
或者 superView.top
了。
当然这个类的作用不止于此,程序员都是追求极致简单的。
我们连 MASConstraintMaker 都不希望在使用的时候自己创建,于是需要给 UIView 添加这么一个方法:
// View+MASAdditions.h
/**
* 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(^)(MASConstraintMaker *make))block;
这就是我们经常使用的那个方法,为了在给一个 view 添加约束的时候看起来整体性更加强,Masonry 将所有的添加约束方法放在了 block 里面进行,于是就有了如下使用:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(10);
make.left.equalTo(superview.mas_left).with.offset(10);
make.bottom.equalTo(superview.mas_bottom).with.offset(-10);
make.right.equalTo(superview.mas_right).with.offset(-10);
}];
这样,其实很容易就能想到这个 mas_makeConstraints:^(MASConstraintMaker *make)
方法是如何实现的:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
当然了,系统比较傻,view 的添加约束方法只有添加
// UIView.h
// UIView (UIConstraintBasedLayoutInstallingConstraints)
- (void)addConstraint:(NSLayoutConstraint *)constraint NS_AVAILABLE_IOS(6_0);
但是问题是,约束重复叠加是会出问题的。因此 Masonry 提供了两个方法:
// View+MASAdditions.h
// 更新现有的约束
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
// 移除旧的约束,并重新添加新的约束
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;
它们的具体实现也不复杂,其实也就是给 MASConstraintmaker 添加两个布尔值,然后在 [constraintMaker install]
的和 [constraint install]
的时候根据布尔值来做对应的操作而已。
这个类其实很简单,就是添加了一个属性来做 Debug。其他没有了。
@property (nonatomic, strong) id mas_key;
至此,我们回头看看,我们已经完成目标了,其实 Masonry 的最关键的几个类也就在这里了,下面还有一些衍生的。
批量给 view 添加约束,数组里面必须是 UIView 类型的,然后统一给所有 view 添加一致的约束。
龟毛的作者,非要简化一下批量添加约束的方法名….
用来做 Debug 用的,这个还是比较有用的,因为官方的 NSLayoutConstraint Debug 跟使用一样恶心。
恩,依然是作者的龟毛,简化一下 View+MASViewAddition 的命名。
其实不只是 UIView, UIViewController 也是可以添加约束的。
总的来看,Masonry 有很多可以学习的地方:
- 链式语法:脑洞大开的使用方式,让代码使用简单并且易读。这需要非常深厚的编码功底才能想的出来。
- 巧用分类:使用分类,配合继承和代理,可以简化很多代码,如果说链式语法在实战中使用还是比较麻烦,那么熟练使用分类、继承、协议,将会让你的代码更上一层楼。
- 思维能力:到现在为止我依然对 Masonry 的作者表示敬佩,非常棒的思维能力,解决了一个大问题。灵活的运用技能会让你在工作的时候事半功倍。
VFL(Visual Format Language) 是官方出的简化版,不过真的很难用,而且代码写起来一点都不优雅。
最后,这里还有一篇解读 Masonry 的文章,写的很好,提供了一个类导图,可以让你更加清除整个 Masonry 的结构。