简介
Masonry是 Objective-C 中用于自动布局的第三方框架, 我们一般使用它来代替冗长, 繁琐的 AutoLayout 代码,它同时支持iOS和OS X。Masonry是一种领域特定语言(DSL),为自动布局的所有功能提供便捷的方法,包括建立和修改约束、存取属性、设置优先级以及调试支持。Masonry的安装推荐使用cocoa Pod方式。有关于pod的使用,将会在另一篇文章中进行说明。
分析
Masonry的使用还是相当简洁的
// CODE1
[button mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.top.equalTo(self.view).with.offset(40);
make.width.equalTo(@185); make.height.equalTo(@38);}];
上边这条代码实现的简要效果是将一个button的大小设置为185*38,居中并距页面顶部为40。而这个效果也可以很容易的从代码中看出,没错,这就是Masonry,是不是很简单?
从mas_makeConstraints:开始
// CODE2
// View+MASAdditions.h
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
这个方法主要是用于约束的第一次构建,从CODE1我们可以看到它在block方法中实现了对调用该方法的的控件的约束。与之相同的,也有用于更新和重构的约束的分类方法:
// CODE3
// View+MASAdditions.h
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;//更新
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;//重构
mas_makeConstraints
只负责新增约束 Autolayout不能同时存在两条针对于同一对象的约束 否则会报错 mas_updateConstraints
针对上面的情况 会更新在block中出现的约束 不会导致出现两个相同约束的情况mas_remakeConstraints
则会清除之前的所有约束 仅保留最新的约束三种函数善加利用 就可以应对各种情况了.
Constraint Maker Block
我们以mas_makeConstraints:
方法为入口来分析一下 Masonry 以及类似的框架(SnapKit)是如何工作的. mas_makeConstraints:
方法位于 UIView
的分类 MASAdditions
中.
Provides constraint maker block and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs.
这个分类为我们提供一种非常便捷的方法来配置 MASConstraintMaker
, 并为视图添加 mas_left
mas_right
等属性.这个方法的主要实现方法为:
// CODE4
// View+MASAdditions.m- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
因为 Masonry 是封装的苹果的 AutoLayout 框架, 所以我们要在为视图添加约束前将translatesAutoresizingMaskIntoConstraints
属性设置为 NO
. 如果这个属性没有被正确设置, 那么视图的约束不会被成功添加.在设置 translatesAutoresizingMaskIntoConstraints
属性之后,
- 我们会初始化一个
MASConstraintMaker
的实例. - 然后将 maker 传入 block 配置其属性.
- 最后调用 maker 的
install
方法为视图添加约束.
MASConstraintMaker
MASConstraintMaker 为我们提供了工厂方法来创建 MASConstraint. 所有的约束都会被收集直到它们最后调用 install 方法添加到视图上.
Provides factory methods for creating MASConstraints. Constraints are collected until they are ready to be installed
在初始化 MASConstraintMaker 的实例时, 它会持有一个对应 view 的弱引用, 并初始化一个 constraints 的空可变数组用来之后配置属性时持有所有的约束.
//CODE5
// MASConstraintMaker.m
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new; return self;
}
这里的 MAS_VIEW
是一个宏, 是 UIView
的 alias.
// CODE6
// MASUtilities.h
#define MAS_VIEW UIView
Setup MASConstraintMaker
在调用 block(constraintMaker)
时, 实际上是对 constraintMaker
的配置.
// CODE7
make.centerX.equalTo(self.view);
make.top.equalTo(self.view).with.offset(40);
make.width.equalTo(@185);
make.height.equalTo(@38);
make.left
访问 make
的 left
right
top
bottom
等属性时, 会调用 constraint:addConstraintWithLayoutAttribute:
方法.
// CODE8
// MASViewConstraint.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 *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
if ([constraint isKindOfClass:MASViewConstraint.class]) { ... }
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}
在调用链上最终会达到 constraint:addConstraintWithLayoutAttribute:
这一方法, 在这里省略了一些暂时不需要了解的问题. 因为在这个类中传入该方法的第一个参数一直为 nil
, 所以这里省略的代码不会执行.这部分代码会先以布局属性 left
和视图本身初始化一个 MASViewAttribute
的实例, 之后使用 MASViewAttribute
的实例初始化一个 constraint
并设置它的代理, 加入数组, 然后返回.这些工作就是你在输入 make.left
进行的全部工作, 它会返回一个 MASConstraint
, 用于之后的继续配置.
make.left.equalTo(@80)
在 make.left
返回 MASConstraint
之后, 我们会继续在这个链式的语法中调用下一个方法来指定约束的关系.
//CODE9
// MASConstraint.h
- (MASConstraint * (^)(id attr))equalTo;
- (MASConstraint * (^)(id attr))greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))lessThanOrEqualTo;
这三个方法是在 MASViewConstraint
的父类, MASConstraint
中定义的.MASConstraint
是一个抽象类, 其中有很多的方法都必须在子类中覆写的. Masonry 中有两个 MASConstraint
的子类, 分别是 MASViewConstraint
和 MASCompositeConstraint
. 后者实际上是一些约束的集合. 这么设计的原因我们会在 post 的最后解释.先来看一下这三个方法是怎么实现的:
// CODE10
// MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
该方法会导致 self.equalToWithRelation
的执行, 而这个方法是定义在子类中的, 因为父类作为抽象类没有提供这个方法的具体实现.
// CODE11
// MASConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
MASMethodNotImplemented();
}
MASMethodNotImplemented
也是一个宏定义, 用于在子类未继承这个方法或者直接使用这个类时抛出异常.
// CODE12
// MASConstraint.m
#define MASMethodNotImplemented() \ @throw [NSException exceptionWithName:NSInternalInconsistencyException \ reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \ userInfo:nil]
因为我们为 equalTo
提供了参数 attribute
和布局关系 NSLayoutRelationEqual
, 这两个参数会传递到 equalToWithRelation
中, 设置 constraint
的布局关系和 secondViewAttribute
属性, 为即将 maker 的 install
做准备.
// CODE13
// MASViewConstraint.m
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) { ... }
else { ... self.layoutRelation = relation;
self.secondViewAttribute = attribute; return self;
}
};
}
我们不得不提一下 setSecondViewAttribute:
方法, 它并不只是一个简单的 setter 方法, 它会根据你传入的值的种类赋值.
// CODE14
// MASConstraintMaker.m
- (void)setSecondViewAttribute:(id)secondViewAttribute {
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
[self setLayoutConstantWithValue:secondViewAttribute];
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
_secondViewAttribute = secondViewAttribute;
} else {
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
}
第一种情况对应的就是:
// CODE15
make.left.equalTo(@40);
传入 NSValue
的时, 会直接设置 constraint
的 offset
, centerOffset
, sizeOffset
, 或者 insets
第二种情况一般会直接传入一个视图:
// CODE16
make.left.equalTo(view);
这时, 就会初始化一个 layoutAttribute
属性与 firstViewArribute
相同的 MASViewAttribute
, 上面的代码就会使视图与 view
左对齐.第三种情况会传入一个视图的 MASViewAttribute:
// CODE17
make.left.equalTo(view.mas_right);
使用这种写法时, 一般是因为约束的方向不同. 这行代码会使视图的左侧与 view
的右侧对齐.到这里我们就基本完成了对一个约束的配置, 接下来可以使用相同的语法完成对一个视图上所有约束进行配置, 然后进入了最后一个环节.
Install MASConstraintMaker
我们会在 mas_makeConstraints:
方法的最后调用 [constraintMaker install]
方法来安装所有存储在 self.constraints
数组中的所有约束.
// CODE18
// MASConstraintMaker.m
- (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
, 如果我们在最开始调用 mas_remakeConstraints:
方法时, 视图中原来的约束就会全部被 uninstall
.然后就会遍历 constraints
数组, 发送 install
消息.
MASViewConstraint install
MASViewConstraint 的 install
方法就是最后为当前视图添加约束的最后的方法, 首先这个方法会先获取即将用于初始化 NSLayoutConstraint
的子类的几个属性.
// CODE19
// MASViewConstraint.m
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.view;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.view;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
Masonry 之后会判断当前即将添加的约束是否是 size 类型的约束
// CODE20
// MASViewConstraint.m
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = firstLayoutItem.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
如果不是 size 类型并且没有提供第二个 viewAttribute
, (e.g. make.left.equalTo(@10)
;) 会自动将约束添加到 superview
上. 它等价于:
// CODE21
make.left.equalTo(superView.mas_left).with.offset(10);
然后就会初始化 NSLayoutConstraint
的子类 MASLayoutConstraint:
// CODE22
// MASViewConstraint.mMASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:
firstLayoutItem attribute:firstLayoutAttribute relatedBy:
self.layoutRelation toItem:
secondLayoutItem attribute:
secondLayoutAttribute multiplier:
self.layoutMultiplier constant:
self.layoutConstant];
layoutConstraint.priority = self.layoutPriority;
接下来它会寻找 firstLayoutItem
和 secondLayoutItem
两个视图的公共 superview
, 相当于求两个数的最小公倍数.
// CODE23
// View+MASAdditions.m
- (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;
}
如果需要升级当前的约束就会获取原有的约束, 并替换为新的约束, 这样就不需要再次为 view 安装约束.
// CODE24
// MASViewConstraint.m
MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
// just update the constant existingConstraint.constant = layoutConstraint.constant;
self.layoutConstraint = existingConstraint;
} else {
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
}
[firstLayoutItem.mas_installedConstraints addObject:self];
如果原来的 view 中不存在可以升级的约束, 或者没有调用 mas_updateConstraint: 方法, 那么就会在上一步寻找到的 installedView 上面添加约束.
// CODE25
[self.installedView addConstraint:layoutConstraint];
方法及属性
首先列举一些Masonry的属性:
// CODE26
@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;
这些属性与NSLayoutAttrubute
的对照表如下:
Masonry | NSAutoLayout | 说明 |
---|---|---|
left | NSLayoutAttributeLeft | 左侧 |
top | NSLayoutAttributeTop | 上侧 |
right | NSLayoutAttributeRight | 右侧 |
bottom | NSLayoutAttributeBottom | 下侧 |
leading | NSLayoutAttributeLeading | 首部 |
trailing | NSLayoutAttributeTrailing | 尾部 |
width | NSLayoutAttributeWidth | 宽 |
height | NSLayoutAttributeHeight | 高 |
centerX | NSLayoutAttributeCenterX | 横向中点 |
centerY | NSLayoutAttributeCenterY | 纵向中点 |
baseline | NSLayoutAttributeBaseline | 文本基线 |
其中leading与left trailing与right 在正常情况下是等价的,但是当一些布局是从右至左时(比如阿拉伯文?没有类似的经验) 则会对调,换句话说就是基本可以不理不用 用left和right就好了.
此外,除了上边提到的mas_makeConstraints
、 mas_updateConstraints
和 mas_remakeConstraints
这三种构建约束的方法,Masonry还有一些其他方法,下面将进行简要介绍:
// CODE27
- (MASConstraint * (^)(id))greaterThanOrEqualTo;//大于等于
- (MASConstraint * (^)(id))mas_greaterThanOrEqualTo;//大于等于
- (MASConstraint * (^)(id))lessThanOrEqualTo;//小于等于
- (MASConstraint * (^)(id))mas_lessThanOrEqualTo;//小于等于
- (MASConstraint * (^)())priorityLow;//优先级低
- (MASConstraint * (^)())priorityMedium;//优先级中
- (MASConstraint * (^)())priorityHigh;//优先级高
- (MASConstraint * (^)(CGFloat))multipliedBy;//比例
使用
说到使用,其实我还是认为Masonry在github上给出的官方demo是最好的参考,并且完全达到了能使人灵活使用的程度。下面是下载链接:
https://github.com/SnapKit/Masonry
这里就不再进行赘述。
注意事项
在使用Masonry的过程中,总会出现一些意想不到的问题导致我们的程序Crash,所以说,下边总结出了一些注意事项。
equal和mas_equal的区别
equalTo
和 mas_equalTo
的区别在哪里呢? 其实 mas_equalTo
是一个MACRO
// CODE28
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))
可以看到 mas_equalTo
只是对其参数进行了一个BOX操作(装箱) MASBoxValue的定义具体可以看看源代码 太长就不贴出来了。它所支持的类型 除了NSNumber
支持的那些数值类型之外 就只支持CGPoint
CGSize
UIEdgeInsets
。而equalTo
则主要是对对象及属性的赋值。也就是说,当我们括号内的参数为某一具体数值时,需要用mas_equalTo
, 当参数为对象或者属性时,需要用equalTo
。
关于括号内参数数值的正负
有时候我们会发现括号内的参数为负数,这是因为计算的是绝对的数值,具体是取正还是取负,这个由屏幕的坐标增长方向决定。
and&with
// CODE29
- (MASConstraint *)with { return self;}
- (MASConstraint *)and { return self;}
由此可以看出,这两个函数其实什么都没有做,加入这个的目的,就是让代码看起来更加的自然。
关于父视图的问题
当一个控件被添加到其父视图上后才可以进行约束,并且值得注意的是,在iOS7中,一旦子视图利用了它爷爷(父视图的父视图)或者它叔伯(父视图的同级视图)进行约束,那么,程序将会crash。
总结
Masonry 与其它的第三方开源框架一样选择了使用分类的方式为 UIKit 添加一个方法 mas_makeConstraint
, 这个方法接受了一个 block, 这个 block 有一个 MASConstraintMaker
类型的参数, 这个 maker 会持有一个约束的数组, 这里保存着所有将被加入到视图中的约束.
我们通过链式的语法配置 maker, 设置它的 left
right
等属性, 比如说 make.left.equalTo(view)
, 其实这个 left equalTo
还有像 with offset
之类的方法都会返回一个 MASConstraint
的实例, 所以在这里才可以用类似 Ruby 中链式的语法.在配置结束后, 首先会调用 maker 的 install
方法, 而这个 maker 的 install
方法会遍历其持有的约束数组, 对其中的每一个约束发送 install
消息.
在这里就会使用到在上一步中配置的属性, 初始化 NSLayoutConstraint
的子类 MASLayoutConstraint
并添加到合适的视图上.视图的选择会通过调用一个方法 mas_closestCommonSuperview:
来返回两个视图的最近公共父视图.