作者:伯乐在线 - 小笨狼
链接:http://ios.jobbole.com/83384/
key
当约束冲突发生的时候,我们经常为找不到是哪个View冲突的而烦恼,这一堆View是个什么东西呀?
"<MASLayoutConstraint:0x7f8de483fb10 UIView:0x7f8de2f53870.left == UIView:0x7f8de2f586c0.left>",
"<MASLayoutConstraint:0x7f8de4818b50 UIView:0x7f8de2f53870.right == UIView:0x7f8de2f586c0.right>",
"<MASLayoutConstraint:0x7f8de4818870 UIView:0x7f8de2f53870.width == 100>",
"<NSLayoutConstraint:0x7f8de4847e90 UIView:0x7f8de2f586c0.width == 375>"
Will attempt to recover by breaking constraint
<MASLayoutConstraint:0x7f8de4818870 UIView:0x7f8de2f53870.width == 100>
这时候我们可以设置View的key:
self.view.mas_key = @"self.view";
view1.mas_key = @"view1";
设置之后再看一下,哈哈,现在好多了。可以清晰的知道是哪个view了
"<MASLayoutConstraint:0x7fcd98d17c40 UIView:view1.left == UIView:self.view.left>",
"<MASLayoutConstraint:0x7fcd98d2b2c0 UIView:view1.right == UIView:self.view.right>",
"<MASLayoutConstraint:0x7fcd98d2adb0 UIView:view1.width == 100>",
"<NSLayoutConstraint:0x7fcd98e42050 UIView:self.view.width == 375>"
Will attempt to recover by breaking constraint
<MASLayoutConstraint:0x7fcd98d2adb0 UIView:view1.width == 100>
大家可能会觉得这样一个一个设置,多麻烦啊!别着急,Masonry提供了批量设置的宏MASAttachKeys 只需要一句代码即可全部设置:
MASAttachKeys(self.view,view1);
Shorthand(12月7日新增)
在写代码的时候,可能你会感觉有的东西要加mas前缀,有的东西又不用加,代码风格不统一,而且加mas前缀还麻烦。
前面介绍过加mas前缀主要是在扩展系统类的时候为了避免与原有类冲突,这是Apple推荐的做法。不过目前来说,即使不加mas前缀,也不会有什么问题。所以Masonry提供了不加mas_前缀的方法,只需要你定义几个宏即可。
1. MAS_SHORTHAND
定义MASSHORTHAND宏之后。可以使用UIView,NSArray中不带mas前缀的makeConstraints,updateConstraints,remakeConstraints。以及UIView中不带mas_前缀的Attribute。
2. MAS_SHORTHAND_GLOBALS
默认的equalTo方法只接受id类型的对象。有时候我们想传入一个CGFloat, CGSize, UIEdgeInsets等。还需要将其转化成NSValue对象,比较麻烦。Masonry也考虑到了这种情况。只需要定义MAS_SHORTHAND_GLOBALS宏。就可以直接对equalTo传入基础类型。Masonry自动转化成NSValue对象
拨开Masonry的衣服
Masonry的基本使用方法介绍完了,那么我们来看看Masonry的内部到底有些什么东西?
结构
Masonry一共有十三个类,我将这13个类分为5个模块:
Help
Help模块主要是一些辅助的类。
NSLayoutConstraint+MASDebugAdditions:这个类的主要作用是重写NSLayoutConstraint的description函数。让约束发生冲突的时候,更易读。如果View或者constraint设置了Key,直接用key的值显示到description中。如果没有设置,显示View或者constraint的指针。
ViewController+MASAdditions:提供了ViewController的LayoutGuide相关属性,以便View对齐时使用 MASUtilities:定义了一些公用的宏和属性
Shorthand
对于系统原有类(NSArray,UIView)的扩展。Masonry的category方法和属性都加有mas前缀。这也是Apple建议的做法,避免跟系统原有方法冲突。但是有时候我们可能想用的更方便,不想写mas前缀(没办法,我就是这么懒…)
在NSArray+MASShorthandAdditions和View+MASShorthandAdditions中定义了不带mas前缀的扩展。这些扩展根据你是否定义了MAS_SHORTHAND宏来确定是否编译。所以你只需要定义MAS_SHORTHAND宏,就可以方便的使用不带mas前缀的方法,比如:-[view makeConstraints:]
Public
Public模块主要是对外暴露的方法。使用者使用Masonry可以直接接触到。
- NSArray+MASAdditions:主要有定义和更新约束的方法,如mas_makeConstraints:
- View+MASAdditions:除了定义和更新约束的一系列方法之外,还为View增加了mas_top, mas_left等Attribute属性
Core
Core模块就是Masonry的核心部分,Masonry的大部分功能都在这4个类里实现
- MASConstraintMaker:约束控制器。控制更新,删除,或者新增约束
- MASConstraint:约束的基类,虚类。定义了Constraint的基本属性和方法。
- MASViewConstraint: 约束的主要实现类。所有对约束使用的功能均在此类中完成
- MASCompositeConstraint:约束的集合类。内部有一个数组,可以保存多个MASViewConstraint。对MASCompositeConstraint调用方法实际等于对其内部的所有MASViewConstraint调用方法
Property
此模块主要封装了一些MASConstraint持有的属性。为了使用更方便,或者扩展功能
- MASViewAttribute:每一个Attribute都有一个View与之对应,为了使用更方便,所以将他们通过一个类封装在一起
- MASLayoutConstraint:默认的NSLayoutConstraint是没有Key这个属性的,为了Debug方便。派生一个子类,持有key属性
实现
当我们给View添加一个约束的时候到底发生了什么?
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.equalTo(view1.superview).offset(20);
}];
我们首先来看make.left.top.equalTo(view1.superview).offset(20);
一、执行”make.left”
MASConstraintMaker类中有一个属性constraints专门用来存储constraint
@property (nonatomic, strong) NSMutableArray *constraints;
当执行make.left的时候, 会将相应的MASConstraint添加到constraints数组中
- (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];
//调用make.left.top时走入这里将原来的ViewConstraint替换成MASCompositeConstraint
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;
}
// 调用make.left的时候走入这里,将constraint加入到self.constraints中
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}
对MASConstraintMaker调用Attribute的get方法,最终都会走到-constraint:addConstraintWithLayoutAttribute:中,在这个方法中,通过对应的Attribute生成MASViewConstraint。然后将MASViewConstraint加入到constraints中
二、执行”.top”
make.left返回的是MASConstraint类型。所以make.left.top是对MASViewConstraint类型调用top方法。
- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
当执行-addConstraintWithLayoutAttribute的时候,ViewConstraint通过delegate又调回到MASConstraintMaker的-constraint:addConstraintWithLayoutAttribute:中。
在MASConstraintMaker的-constraint:addConstraintWithLayoutAttribute:里,将原来constraints中的MASViewConstraint替换成MASCompositeConstraint。MASCompositeConstraint持有top,left 2个属性。对MASCompositeConstraint做操作时候,其内部的所有属性都会执行相应的操作
三、执行”.equalTo(view1.superview)”
- (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.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;
}
};
}
当执行Relationship的方法时,都会走到-equalToWithRelation中。 在这个方法里面主要是给realationship和secondViewAttribute赋值:
1. 如果不是数组
直接对realationship和secondViewAttribute赋值
2. 如果是数组
如: .equalTo(@[view1.mas_left,view2.mas_left]),逻辑上肯定不能是不等关系(>=,<=),所以realationship不用赋值,使用默认值(=)。copy出多个viewConstraint,将secondViewAttribute赋值。然后用多个viewConstraint组成的compositeConstraint替换调原来的viewConstraint。
四、执行”.offset(10)”
- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}
- (void)setOffset:(CGFloat)offset {
self.layoutConstant = offset;
}
offset(10)会将10传入到ViewConstraint中,用layoutConstant属性将其存起来。(offset主要影响的是约束里面的constant)
五、mas_makeConstraints
看完了make.left.top.equalTo(view1.superview).offset(20);,我们再看看mas_makeConstraints中到底做了什么?
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
mas_makeConstraints方法很简单
- 将self.translatesAutoresizingMaskIntoConstraints至为NO。translatesAutoresizingMaskIntoConstraints表示是否将设置的Frame转化为约束。当自己设置约束的时候需要将其置为NO
- 创建出MASConstraintMaker对象
- 通过block抛出到外面设值
- constraintMaker install
上面的代码我们知道,关键的地方还是在于constraintMaker install
六、constraintMaker install
- (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;
}
- 如果需要removeExisting,就把已有的约束remove掉,当调用mas_remakeConstraints的时候会将removeExisting值置为YES
- 遍历constraints,调用[constraint install]
- 清空constraints,这里的constraintMaker只是一个零时属性,只是一个工具类,不需要存储。所以用完之后就可以将constraints清空
其实真正关键的地方在[constraint install]
七、constraint install
- (void)install {
// 1. 已经installed的将不做任何操作
if (self.hasBeenInstalled) {
return;
}
//2. 从ViewAttribute中剥离出item和attribute
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
//3. 如果没有secondViewAttribute,默认secondItem为其父View,secontAttribute等于firstLayoutAttribute。
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}
//4. 创建真正用于Autolayout的约束layoutConstraint
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
//5. 将priority和key赋值
layoutConstraint.priority = self.layoutPriority;
layoutConstraint.mas_key = self.mas_key;
//6. 找到要添加约束的installView
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;
}
//7. 添加或更新约束
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];
}
}
- 如果已经installed就不做任何操作
- 从ViewAttribute中剥离出item和attribute。前面我们介绍过MASViewAttribute的类主要是将item和attribute2个属性封装在了一起。
- secondViewAttribute的值来自于.equalTo(item.attribute)中的item.attribute。当我们写下类似make.left.offset(10);约束的时候,是没有secondViewAttribute的,这时候默认secondItem为其父View,secontAttribute等于firstLayoutAttribute。这就解释了为什么可以这样写make.left.offset(10);
- 创建真正用于Autolayout的约束layoutConstraint
- 将priority和key赋值
- 找到要添加约束的installView。如果是2个View之间的约束,需要寻找这2个View最接近的共同父View。添加约束
- 添加或更新约束。当调用mas_updateConstraints的时候updateExisting=YES。这时候会查找是否有已经存在的约束。有就更新,没有就添加。如果是mas_makeConstraints或mas_remakeConstraints,则直接添加
Extension
仅仅将代码结构和基本实现过程解析了一下,更多实现细节还需要大家自己去阅读源码
说实话,Masonry的代码写得真漂亮,不管是代码格式规范,还是设计模式。看起来简直是一种享受。建议大家阅读。
参考
- Masonry源码
- Autolayout的第一次亲密接触
- iOS8上关于UIView的Margin新增了3个APIs