iOS链式语法 & Masonry的实现
Demo:https://github.com/BaiKunlun/chained-syntax-implementation-with-Object-C
Masonry源码:https://github.com/desandro/masonry
Object-C的语法一直被诟病,不知是从什么时候开始,链式语法的出现提供了一种更加优雅的实现。目前由很多的工具库都采用了“链式语法”,其中就包括苹果的AutoLayout和对它进行二次封装的Masonry工具库。那么如何通过OC来实现链式语法呢?Masonry的又是如何巧妙的实现对Autolayout的封装呢?进入正题。
1、什么是链式语法
下面这段代码就是一段用Masonry来实现自动布局,可以看到链式语法没有OC标志性的“[]”,取而代之的是“.”。这种多级的调用,好像在写一个句子,使得代码的语义更加清晰明了,不失为一种优雅的实现。
[self.contentView mas_remakeConstraints:^(MASConstraintMaker *make) {
make.centerX.mas_equalTo(self.view.mas_centerX);
make.centerY.mas_equalTo(self.view.mas_centerY).with.offset(20);
make.width.mas_equalTo(300);
make.height.mas_equalTo(200);
}];
2、链式语法的实现
其实对于这种调用方式我们并不陌生,OC里对于属性的访问就是通过“.”来实现的。那么第一反应就是。所以我们可以通过属性的方式,来实现多级的调用。比如:
@interface ClassA : NSObject
@property (nonatomic, strong) ClassB *b;
@end
@interface ClassB : NSObject
@property (nonatomic, strong) ClassC *c;
@end
@interface ClassC : NSObject
@end
我们创建了ClassA、ClassB、ClassC三个类,C是B的一个属性,B是A的一个属性。那么此时我们就可以对一个A的实例通过Object.b.c的方式方式来进行访问,这看上去跟链式语法很像,但是不同的是,这只是对属性的访问,并没有传参。通过Masonry的例子可以看到,参数通过“(XXX)”的方式进行设置。这种方式并不是OC里的函数传参方式,唯一的解释是Block。
Block在iOS开发里面有大量的运用,其语法更像是C。既然这种传参的方式是block特有的,那么可以断定,访问的这个“属性”其实是一个block。我们对上面的语法进行改造:
@interface ClassA : NSObject
@property (nonatomic, readonly) ClassB *(^setB)(NSInteger count);
@end
@implementation ClassA
- (ClassB *(^)(NSInteger))setB
{
return ^(NSInteger count) {
ClassB *b = [ClassB new];
b.count = count;
return b;
};
}
@end
@interface ClassB : NSObject
@property (nonatomic) NSInteger count;
@end
ClassA *a = [ClassA new];
ClassB *b = a.setB(100);
可以看到ClassA中增加了一个setB的属性,返回的是一个带有参数的block,调用时通过传参可以创建一个ClassB对象,对其进行赋值。最为重要的是,block的返回是一个ClassB对象。个人认为链式语法的巧妙之处有两个,一个是将block作为属性进行访问,第二个就是block的返回参数可以是自定义的对象,而这两种特性使得链式访问成为了可能。试想,如果我们对ClassB进行如同ClassA的改造,那么我们就可以实现如同a.setB(100).setC(200)这样的语法形式,而这就是链式语法的核心。如果访问的属性没有传参,那么可以是一个实例属性的访问,而如果进行了传参,那么一定是通过block实现的。
当然,不是每次访问属性返回的都一定是一个新的对象,有时候,我们为了保证链式语法在语义上的优势,需要一些语义上的过渡,可以通过控制block返回的实例来控制语法链的调用,可以返回self,也可以返回一个新的实例,这个实例往往是当前self的一个成员变量。下面通过一个实际的例子来理解一下链式语法的使用:
@interface Car : NSObject
@property (nonatomic, strong) window *window;
@property (nonatomic, strong) wheel *wheel;
@end
@implementation Car
- (instancetype)init
{
self = [super init];
if (self) {
_window = [window new];
_wheel = [wheel new];
}
return self;
}
@end
@interface window : NSObject
@property (nonatomic, strong) UIColor *color;
@property (nonatomic, readonly) window *(^drawColor)(UIColor *color);
@end
@implementation window
- (window *(^)(UIColor *))drawColor
{
return ^(UIColor *color) {
self.color = color;
return self;
};
}
@end
@interface wheel : NSObject
@property (nonatomic) NSInteger size;
@property (nonatomic, readonly) wheel *(^sizeEqualTo)(NSInteger num);
@end
@implementation wheel
- (wheel *(^)(NSInteger))sizeEqualTo
{
return ^(NSInteger num) {
self.size = num;
return self;
};
}
@end
Car *myCar = [Car new];
myCar.window.drawColor([UIColor redColor]);
myCar.wheel.sizeEqualTo(10);
有一个Car类,包含了两个属性window和wheel,通过调用window和wheel的block属性来实现对window和wheel的设置。
3、Masonry的实现
现在我们已经一步一步的接近Masonry的样式了,但是还有一定的差距,那我们就要看一下Masonry的源码实现了。
传送门:https://github.com/desandro/masonry
首先看一下Masonry.h头文件
//
// Masonry.h
// Masonry
//
// Created by Jonas Budelmann on 20/07/13.
// Copyright (c) 2013 cloudling. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for Masonry.
FOUNDATION_EXPORT double MasonryVersionNumber;
//! Project version string for Masonry.
FOUNDATION_EXPORT const unsigned char MasonryVersionString[];
#import "MASUtilities.h"
#import "View+MASAdditions.h"
#import "View+MASShorthandAdditions.h"
#import "ViewController+MASAdditions.h"
#import "NSArray+MASAdditions.h"
#import "NSArray+MASShorthandAdditions.h"
#import "MASConstraint.h"
#import "MASCompositeConstraint.h"
#import "MASViewAttribute.h"
#import "MASViewConstraint.h"
#import "MASConstraintMaker.h"
#import "MASLayoutConstraint.h"
#import "NSLayoutConstraint+MASDebugAdditions.h"
可以看到,Masonry除了自定义了一些功能类之外,还对UIView、UIViewController、NSArray进行了扩展。这也就解释了为什么我们可以在视图对象上直接调用masonry方法来进行自动布局。下面对Masonry与我们当前实现的链式语法之间的不同分别进行回答。
问题1:为什么要使用mas_remakeConstraints等方法将约束的内容放在一个block中实现?
个人认为此处是Masonry实现的一个精妙之处,我们上面实现的链式语法,在进行链式调用的时候就会“立即生效”,而Masonry在实现AutoLayout布局时的不同之处在于,它需要获得开发者对该视图的“所有约束”之后,统一进行布局。我们看一下mas_makeConstraints的实现:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
首先关闭了视图的Autoresizie,然后创建了一个constraintMaker类,可以肯定,这是Masonry自定义的视图布局类。然后将constraintMaker实例以参数的形式block回传,这就是我们在使用时看到的make。我们的系列约束,实际上都是在对constraintMaker实例进行参数的设置。当我们在block中设置完constraintMaker之后,此时回到mas_makeConstraints的最后一句,将constraintMaker设置的布局,统一生效(install)。
问题2:constraintMaker是如何对视图的各个属性进行设置的?
WX20170416-160914@2x.png
可以看到,constraintMaker包含了left、top等所有布局中能够用到的约束属性,这些实例属性都是由MASConstraint基类衍生的,并不是block实现。而MasConstraint下的实现,就涉及到“传参”,还是那句话,一旦有传参,那么一定是block。事实也正是如此,以priorityLow为例:
- (MASConstraint * (^)())priorityLow {
return ^id{
self.priority(MASLayoutPriorityDefaultLow);
return self;
};
}
其将priority属性进行设置,但返回的仍然是MASConstraint对象,所以开发者可以继续调用MASConstraint下的其它属性进行设置。但并不是所有的属性都是这么简单的,比如equalTo:
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
在Masonry中,equalTo的传参可以是具体的数值,也可以是其它视图的属性。所以block中的传参是一个id,而这个参数通过equalToWithRelationcan调用传递了下去。equalToWithRelationcan接受MASViewAttribute, UIView, NSValue, NSArray类型的传参,这也使得我们在布局的时候异常灵活方便。那么equalToWithRelationcan里是如何处理传参的呢?
- (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.layoutRelation = relation;
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;
}
};
}
可以看到,对不同类型的传参进行了不同处理。如果是NSArray,实际上是一个UIView的数组,那么讲视图关系进行拆分,转换成MASViewConstraint数组,生成MASCompositeConstraint实例,然后返回。如果是其它类型(我们在使用过程中,往往是直接传递一个视图、固定值、或视图的mas属性),则直接对相关属性进行赋值。这些参数将在install的时候被统一解析,并转换为Autolayout语法。
可能有人会问,为什么会有NSArray类型的传参?可以看一下NSArray的扩展,当我们在传参时是允许使用一个UIView数组的。这也是Masonry强大的一点,可以对UIView数组中的每一个视图进行统一布局。
3、布局参数是如何解析成AutoLayout的?
这个问题应该不难,我们在block中进行布局,实际上就是对当前MASConstraintMaker的约束属性进行赋值,它被存放在constraints中,是一个约束的数组。当执行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;
}
MASViewConstraint描述了一个视图属性的所有可能关系,被赋予固定值或与其它视图的某个属性有某种关系,这些都可以通过MASViewAttribute进行模型的建立,可以从[MASViewConstraint install]方法中看到与AutoLayout之间的转换方法,此处不再赘述。
总结:链式语法实际上是将block作为属性访问的一种巧妙运用,当需要进行传参时一定是要通过block来实现的。利用链式语法,我们可以实现很多巧妙的实例调转,而开发者无需关心此时是调用了哪个实例的方法。
第一次写帖子,欢迎大家批评指正。