Masonry一直是OC中优秀的Auto Layout框架,尤其是其优雅的点链式语法设计,为人津津乐道。
今天我们来看看Masonry的源码,看看给我们什么启示。
先从脑海简单想想,如果让我们自己打造一个Auto Layout框架,大概要怎么做?
构建约束
系统的构建约束方法,诉我直言,非常不优雅。VFL依然和Masonry比起来依然糟糕透了。(Masonry不是基于VFL的封装)
Apple貌似无心去优化Auto Layout的代码语法,而是不遗余力去推广Storyboard。添加约束
系统有,简单封装下,如下图。修改约束
系统有,简单封装下,如下图。记录约束
系统有,简单封装下,如下图。
系统接口:
看来最主要还是构建约束的方法,需要封装成一套更加优雅,简单好用的方法。
VFL显然不符合我们面向对象的习惯,像UI版的SQLite。
那只剩唯一的方法:
按照view1.attr1 = view2.attr2 * multiplier + constant来构建约束。
然后进一步抽象成我们约束,形成一套我们构建方法。
简单脑洞就到这吧,我们看看Masonry是怎么做的。
MASViewConstraint - 属于Masonry约束
如view1.attr1 = view2.attr2 * multiplier + constant构建形式。
MASViewAttribute 将一个item/view 和 attribute封装成一个对象。
那么MASViewConstraint 含有 firstViewAttribute和secondViewAttribute。
当然也有可能只有firstViewAttribute,例如对宽或高等于某个具体的数值的约束。
还差multiplier,constant 和 relation(=),priority也不能忘记,尽管它不在式子里。
继续往下看,到父类MASConstraint。
MASConstraint - 优雅的点链式语法
-
multiplier
-
constant
-
relation
-
priority
点链式语法,利用OC中get(即点)方法获取block,这个block执行后又能放回self对象本身。当这个类有多个这种方法时,就能形成点链式的语法。
Talk is Cheap,上代码。例如,改进UIButton的设置:
@interface UIButton (Chain)
- (UIButton *(^)(NSString *normalTitle))normalTitle;
- (UIButton *(^)(NSString *selectedTitle))selectedTitle;
- (UIButton *(^)(UIColor *normalTitleColor))normalTitleColor;
- (UIButton *(^)(UIColor *selectedTitleColor))selectedTitleColor;
- (UIButton *(^)(NSString *normalImageName))normalImageName;
- (UIButton *(^)(NSString *selectedImageName))selectedImageName;
- (UIButton *(^)(CGFloat fontSize))fontSize;
@end
或者我们还可以通过运行时或者继承,将state记录下来,进一步优化接口:
@interface YZChainButton : UIButton
- (YZChainButton *(^)(UIControlState state))yz_state;
- (YZChainButton *(^)(NSString *title))yz_title;
- (YZChainButton *(^)(UIColor *color))yz_titleColor;
- (YZChainButton *(^)(NSString *name))yz_imageName;
- (YZChainButton *(^)(CGFloat fontSize))yz_fontSize;
@end
那就这样实现:
@interface YZChainButton ()
@property (nonatomic, assign) UIControlState yz_controlState;
@end
@implementation YZChainButton
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
- (YZChainButton *(^)(UIControlState state))yz_state {
return ^id (UIControlState state) {
self.yz_controlState = state;
return self;
};
}
- (YZChainButton *(^)(NSString *title))yz_title {
return ^id (NSString *title) {
[self setTitle:title
forState:self.yz_controlState];
return self;
};
}
Demo在此,请戳我。
简单发散一下,我们回到Masonry来继续看,MASConstraint.m
单个到组的拓展 - 使用同一个基类,同一套接口
这些方法都没有实现,由子类去实现,为什么?
最核心也最简单的原因,因为Masonry有多个子类,它们各自的实现又不同。
为什么这些接口要父类来定义,交给各子类各自去定义去实现不就好了?
因为外部使用的时候希望,使用统一(即父类)的接口来调用,而不用关心子类的类型。
来,我们来看MASConstraint的子类们:
MASCompositeConstraint,定义了一组MASConstraint。
MASViewConstraint,定义一个约束。
Masonry 一套构建接口,可以构建多个约束或者一个接口:
按照MASViewConstraint的构建,我们会这样写。
[likeButton2 mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.centerY.equalTo(self.view).offset(150);
}];
有了MASCompositeConstraint,我们可以这样:
[likeButton2 mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view).centerOffset(CGPointMake(0, 150));
}];
实际上,在Masonry内部添加约束都是一组(MASCompositeConstraint)来添加的。 (哈哈,必须是啊,难道还if-else ?!?)
一句代码构建多个约束,一次大大简化,接口却和构建一个约束一毛一样,666。
我们可以在MASConstraintMaker.m文件中看到。
- (MASConstraint *)addConstraintWithAttributes:(MASAttribute)attrs {
__unused MASAttribute anyAttribute = (MASAttributeLeft | MASAttributeRight | MASAttributeTop | MASAttributeBottom | MASAttributeLeading
| MASAttributeTrailing | MASAttributeWidth | MASAttributeHeight | MASAttributeCenterX
| MASAttributeCenterY | MASAttributeBaseline
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
| MASAttributeFirstBaseline | MASAttributeLastBaseline
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
| MASAttributeLeftMargin | MASAttributeRightMargin | MASAttributeTopMargin | MASAttributeBottomMargin
| MASAttributeLeadingMargin | MASAttributeTrailingMargin | MASAttributeCenterXWithinMargins
| MASAttributeCenterYWithinMargins
#endif
);
NSAssert((attrs & anyAttribute) != 0, @"You didn't pass any attribute to make.attributes(...)");
NSMutableArray *attributes = [NSMutableArray array];
if (attrs & MASAttributeLeft) [attributes addObject:self.view.mas_left];
if (attrs & MASAttributeRight) [attributes addObject:self.view.mas_right];
if (attrs & MASAttributeTop) [attributes addObject:self.view.mas_top];
if (attrs & MASAttributeBottom) [attributes addObject:self.view.mas_bottom];
if (attrs & MASAttributeLeading) [attributes addObject:self.view.mas_leading];
if (attrs & MASAttributeTrailing) [attributes addObject:self.view.mas_trailing];
if (attrs & MASAttributeWidth) [attributes addObject:self.view.mas_width];
if (attrs & MASAttributeHeight) [attributes addObject:self.view.mas_height];
if (attrs & MASAttributeCenterX) [attributes addObject:self.view.mas_centerX];
if (attrs & MASAttributeCenterY) [attributes addObject:self.view.mas_centerY];
if (attrs & MASAttributeBaseline) [attributes addObject:self.view.mas_baseline];
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
if (attrs & MASAttributeFirstBaseline) [attributes addObject:self.view.mas_firstBaseline];
if (attrs & MASAttributeLastBaseline) [attributes addObject:self.view.mas_lastBaseline];
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
if (attrs & MASAttributeLeftMargin) [attributes addObject:self.view.mas_leftMargin];
if (attrs & MASAttributeRightMargin) [attributes addObject:self.view.mas_rightMargin];
if (attrs & MASAttributeTopMargin) [attributes addObject:self.view.mas_topMargin];
if (attrs & MASAttributeBottomMargin) [attributes addObject:self.view.mas_bottomMargin];
if (attrs & MASAttributeLeadingMargin) [attributes addObject:self.view.mas_leadingMargin];
if (attrs & MASAttributeTrailingMargin) [attributes addObject:self.view.mas_trailingMargin];
if (attrs & MASAttributeCenterXWithinMargins) [attributes addObject:self.view.mas_centerXWithinMargins];
if (attrs & MASAttributeCenterYWithinMargins) [attributes addObject:self.view.mas_centerYWithinMargins];
#endif
NSMutableArray *children = [NSMutableArray arrayWithCapacity:attributes.count];
for (MASViewAttribute *a in attributes) {
[children addObject:[[MASViewConstraint alloc] initWithFirstViewAttribute:a]];
}
MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children];
constraint.delegate = self;
[self.constraints addObject:constraint];
return constraint;
}
typedef NS_OPTIONS(NSInteger, MASAttribute) {
MASAttributeLeft = 1 << NSLayoutAttributeLeft,
MASAttributeRight = 1 << NSLayoutAttributeRight,
MASAttributeTop = 1 << NSLayoutAttributeTop,
MASAttributeBottom = 1 << NSLayoutAttributeBottom,
MASAttributeLeading = 1 << NSLayoutAttributeLeading,
MASAttributeTrailing = 1 << NSLayoutAttributeTrailing,
MASAttributeWidth = 1 << NSLayoutAttributeWidth,
MASAttributeHeight = 1 << NSLayoutAttributeHeight,
MASAttributeCenterX = 1 << NSLayoutAttributeCenterX,
MASAttributeCenterY = 1 << NSLayoutAttributeCenterY,
MASAttributeBaseline = 1 << NSLayoutAttributeBaseline,
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
MASAttributeFirstBaseline = 1 << NSLayoutAttributeFirstBaseline,
MASAttributeLastBaseline = 1 << NSLayoutAttributeLastBaseline,
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
MASAttributeLeftMargin = 1 << NSLayoutAttributeLeftMargin,
MASAttributeRightMargin = 1 << NSLayoutAttributeRightMargin,
MASAttributeTopMargin = 1 << NSLayoutAttributeTopMargin,
MASAttributeBottomMargin = 1 << NSLayoutAttributeBottomMargin,
MASAttributeLeadingMargin = 1 << NSLayoutAttributeLeadingMargin,
MASAttributeTrailingMargin = 1 << NSLayoutAttributeTrailingMargin,
MASAttributeCenterXWithinMargins = 1 << NSLayoutAttributeCenterXWithinMargins,
MASAttributeCenterYWithinMargins = 1 << NSLayoutAttributeCenterYWithinMargins,
#endif
};
这种位移枚举值,使用|(或)来组合,然后&(与)来提取,然后遍历构建一组(MASCompositeConstraint)约束。
接口分离
曾经有人问,OC语言钟,想让父类的某个属性只让它的子类访问,而不让其他类访问,可以实现吗? 我回答:不可以。 虽然这个答案没毛病,但称不上优秀。
通过Extension,在一个新的.h文件中声明,仅供子类访问的接口(包括属性),然后在需要访问的子类中引入改.h文件,子类就能访问了,而其他未引入改.h文件的类,肯定是无法访问的。
Masonry 中的 MASConstraint+Private.h 就是这种方式。
Extension的作用,不仅仅在.m文件中声明私有接口吧。
免去外部引用的约束修改。
mas_makeConstraints:
直接添加约束。-
mas_updateConstraints:
删除所有原来的约束,再添加新的约束。
-
mas_updateConstraints:
正常修改constant
更新约束,先找是否有相同的约束,有改constant;没有,则添加此约束。
怎样算相同的约束?
除了constant,其他都必须一样! priority,multiplier,relation都必须一样。意味着想用mas_updateConstraints改priority,那是没有可能的事!
只能改constant!只能改constant!只能改constant!
我就是想改某个约束的priority怎么办?
运行后,进入断言了。Cannot modify constraint priority after it has been installed。
可以看到有效的约束的priority,Masonry压根不允许修改。
我们继续看hasBeenInstalled方法
activate了才有效,那么我们可以先deactivate,再activate。
查下原因:
deactive后,本约束从数组中移除了,失去了引用就销毁了,所以外部引用必须为强引用。
//@property (nonatomic, weak) MASConstraint *likeButton2CenterYOffCn;
@property (nonatomic, strong) MASConstraint *likeButton2CenterYOffCn;
成功更新约束!
最终结论:Masonry的约束修改priority和constant不同!
- 必须添加外部强引用,然后deactive之后,修改priority,再重新active。
-
或者删除该约束,重新添加,如下图代码。
改进Masonry
这个是我对Masonry的槽点,于是我决定改进一下。
修改后兼容下面两种方法的修改priority:
Demo在此,请戳我。
Masonry的约束持有思维,不能和其他方式添加的Auto Layout兼容:
Masonry记录约束的方式,通过View动态绑定NSMutableSet的mas_installedConstraints实现的。
通过搜索发现,只有MASViewConstraint的install方法才有调用,意味着只有Masonry自己创建的约束才会被记录。所以呢,xib或者Storyboard创建的约束,Masonry是不会管的,改不动。
这个是不是可以改进一下呢?
毕竟view的约束我们本来就是可以获取到的:
然后将NSLayoutConstraint转成MASViewConstraint !?!
细想一下使用这种方法来获取约束,无法知道某个约束是针对谁创建了。mas_remakeConstraints:会误移除了别的View的约束,不符合Masonry对View的约束布局思想。
因为make.centerX.equalTo(likeButton2)
是两个View共有的约束,两个View的constraints都包含了这个约束。任意一个View移除了它,就两个都没了。
恩,系统constraints记录的约束,不符合Masonry人性化的思想。
Masonry的思想是:对哪个View创建的约束,由那个View持有。
换言之,make.centerX.equalTo(likeButton2)
这个约束不会被likeButton2持有。这种思维更加自然。
一个显而易见的好处就是:只要没有约束冲突likeButton1总是和likeButton2保持水平中心对齐,不会被likeButton2的布局影响到。
至此,Masonry最核心的几个类都介绍过了,几个Category带过一下吧。
View+MASAdditions
MAS_VIEW的Masonry支持,Masonry可能是UIView或者NSView。
ViewController+MASAdditions
是topLayoutGuide和bottomLayoutGuide的支持。
NSArray+MASAdditions
数组的支持,同时为多个View创建一样的约束。
View+MASShorthandAdditions
和NSArray+MASShorthandAdditions
都是去掉mas_的简写。
关于MASViewAttribute为什么重新isEqual
和hash
方法,可参考文章。
Masonry源码解析到此,有疑问或指出错误,请留言。