iOS Masonry源码学习: 链式语法是如何实现的?

思想

链式语法有什么优势?

  • 链式语法提供了一种清晰、可读的方法调用方式,通过连续的方法链表达代码逻辑,提高了代码的表达力和可维护性。

为什么Masonry会使用链式语法?

  • 因为原生的AutoLayout设置Constraint的代码真的究极冗长,使用链式语法来进行约束设置,不仅能减少代码量,还能提高可读性。

示例代码如下:

// 示例代码,这是部分 viewDidLoad中的代码

    UIView *view1 = [[UIView alloc] init];
    [self.view addSubview:view1]
    UIView *view2 = [[UIView alloc] init];
    [self.view addSubview:view2];
// masonry的约束设置方法
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.left.equalTo(self.view).offset(50);
        make.right.equalTo(self.view.mas_centerX).offset(-70);
        make.bottom.equalTo(self.view.mas_centerY).offset(-100);
    }];

1.首先,我们会想到属性

  • 因为链式语法是通过点语法实现的,而oc中常见的点语法是用于访问属性的,所以我们很自然的会想到使用一层一层的属性来形成点语法。

例如:


// 假设ClassA中有个属性为ClassB *b,而ClassB中有个属性为ClassC *c

ClassA *a = [ClassA new];

a.b.c = ?

  • 当我们尝试去实现之后,发现有一个很大的问题:属性的点语法和上面的例子中的.offset(50)有个很大的差别,那就是没法传值

2.其次,我们想想Block

既然涉及到了传值,那我们肯定得往方法和block上想啊。

而既能传值,又能通过点语法调用的有:
1. Block属性
2. 返回值为Block的方法。

例如:

@interface ClassB : NSObject
@property (nonatomic) NSInteger count;
@end

@interface ClassA : NSObject
// block属性
@property (nonatomic, readonly) ClassB *(^set_count_1)(NSInteger);
// 返回值为block的方法
- (ClassB *(^)(NSInteger))set_count_2;
@end

@implementation ClassB
@end

@implementation ClassA
- (ClassB *(^)(NSInteger))set_count_1 { 
    // 有小伙伴在这里可能会感到疑惑,这里返回的是一个: 参数为integer,返回值为 ClassB的BLock。调用setCount这个getter方法,只是返回这个Block而已,又没有调用这个BLock。
    // 有一说一确实,如果是这样用: id b = a.setCount; 那b确实==block。
    // 但如果这样用:id b = a.setCount(1); 那b就是Block调用后的返回值了。
    return ^(NSInteger count) {
        ClassB *b = [ClassB new];
        b.count = count;
        return b
    };
}

- (ClassB *(^)(NSInteger))set_count_2 { 
    return ^(NSInteger count) {
        ClassB *b = [ClassB new];
        b.count = count;
        return b
    };
}
@end
- (void)viewDidLoad {
    [super viewDidLoad];
    ClassA *a = [ClassA new];
    // 这里就得到了一个b。因为a调用了setCount这个block,传入了50这个参数,然后返回了一个ClassB对象。
    ClassB *b = a.set_count_1(50);
    // 同理,但这个是方法,上面那个是block属性
    b = a.set_count_2(60);
}
  • 这里,我们实现了使用点语法加传参数,只需要再改进一下,就可以得到链式语法了。你是不是会想,在ClassB里面再实现一个类似的block属性,然后再返回一个ClassC之类的东西,然后一直下去,直到某个最终的类?这样可以是可以,但只能规定链必须严格按照这个方向去执行(a->b->c...)。如果我要打乱顺序(a->c->b...),那就不行了。
  • 所以,你肯定也能想到:我们把这些所有的类(ClassA,B,C)都变为同一个抽象类不就行了?Masonry就是这么做的。

3.最后,我们看看Masonry

  • 从上面的示例代码我们可以知道,链式语法是从MASConstraintMaker开始的,所以我们直接去看它的源码。(不用怕它多,提取一下它们的共性就好,不用太用力的看)
// MASConstraintMaker.h 的部分源码

@interface MASConstraintMaker : NSObject

/**
 *        The following properties return a new MASViewConstraint
 *  with the first item set to the makers associated view and the appropriate MASViewAttribute
 */
@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;

#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)

@property (nonatomic, strong, readonly) MASConstraint *firstBaseline;
@property (nonatomic, strong, readonly) MASConstraint *lastBaseline;

#endif

#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)

@property (nonatomic, strong, readonly) MASConstraint *leftMargin;
@property (nonatomic, strong, readonly) MASConstraint *rightMargin;
@property (nonatomic, strong, readonly) MASConstraint *topMargin;
@property (nonatomic, strong, readonly) MASConstraint *bottomMargin;
@property (nonatomic, strong, readonly) MASConstraint *leadingMargin;
@property (nonatomic, strong, readonly) MASConstraint *trailingMargin;
@property (nonatomic, strong, readonly) MASConstraint *centerXWithinMargins;
@property (nonatomic, strong, readonly) MASConstraint *centerYWithinMargins;

#endif
.
.
.
// 只看看部分源码,剩下的可以直接去github上看
@end
  • 我们发现,它的属性都是MASConstraint类型。那我们再去看看它的源码
// MASConstraint.h部分源码

/**
 *        Modifies the NSLayoutConstraint constant,
 *  only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
 *  NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
 */
- (MASConstraint * (^)(MASEdgeInsets insets))insets;

/**
 *        Modifies the NSLayoutConstraint constant,
 *  only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
 *  NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight
 */
- (MASConstraint * (^)(CGFloat inset))inset;

/**
 *        Modifies the NSLayoutConstraint constant,
 *  only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
 *  NSLayoutAttributeWidth, NSLayoutAttributeHeight
 */
- (MASConstraint * (^)(CGSize offset))sizeOffset;

/**
 *        Modifies the NSLayoutConstraint constant,
 *  only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following
 *  NSLayoutAttributeCenterX, NSLayoutAttributeCenterY
 */
- (MASConstraint * (^)(CGPoint offset))centerOffset;

/**
 *        Modifies the NSLayoutConstraint constant
 */
- (MASConstraint * (^)(CGFloat offset))offset;
  • 看到没,出现了大量的block。根据我们前面的联想,我们应该抓住出现block的地方。接着往下看看它们的实现。
// MASConstraint.m部分源码
#pragma mark - NSLayoutConstraint constant proxies

- (MASConstraint * (^)(MASEdgeInsets))insets {
    return ^id(MASEdgeInsets insets){
        self.insets = insets;
        return self;
    };
}

- (MASConstraint * (^)(CGFloat))inset {
    return ^id(CGFloat inset){
        self.inset = inset;
        return self;
    };
}

- (MASConstraint * (^)(CGSize))sizeOffset {
    return ^id(CGSize offset) {
        self.sizeOffset = offset;
        return self;
    };
}

- (MASConstraint * (^)(CGPoint))centerOffset {
    return ^id(CGPoint offset) {
        self.centerOffset = offset;
        return self;
    };
}

- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}
  • 这上面的实现都有一个共同点:把自身的某个属性设置为传入的参数,然后这个block再返回self。这正是链式语法的可以换序的关键。
  • 正是这个return self,使我们可以换序使用点语法:因为return self,在点语法的返回值中你还是当前对象,你还能继续再使用当前对象的点语法。(看不明白的话其实把Masonry pod下来debug一下就好了)

通过return self,我们可以把一个constraint对象的属性用多个点语法语义清晰地设置清楚。

当然,并不是所有所有的点语法的实现都是直接return self。有可能它要通过某个方法去处理一些情况判断(其实最终都还是return self)

例如:

// MASConstraint.m
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
  • 再看看里面这个方法的实现,这个方法在两个子类里其实有两种实现,但是基本一样,我们只看最关键的代码
// MASViewConstraint.m 
- (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;
        }
    };
}
  • if 和 else 两部分的思想基本是一样的。我们只看简单的else。
} else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }

其实很清晰明了,还是跟前面那些方法的实现一样,根据参数设好自己的属性,然后再return self

4.送佛送到西

@interface ClassA : NSObject

@property (nonatomic, assign) NSInteger width;
@property (nonatomic, assign) NSInteger height;
@property (nonatomic, readonly) ClassA *(^withWidth)(NSInteger);
- (ClassA *(^)(NSInteger))andHeight;
@end

@implementation ClassA
- (ClassA * _Nonnull (^)(NSInteger))withWidth {
    return ^(NSInteger width) {
        self.width = width;
        return self;
    };
}
- (ClassA * _Nonnull (^)(NSInteger))andHeight {
    return ^(NSInteger height) {
        self.height = height;
        return self;
    };
}
@end
- (void)viewDidLoad {
    [super viewDidLoad];
    ClassA *a = [ClassA new];
    a.withWidth(50).andHeight(60); // 链式语法在这,这两个点语法反序调用也一样的结果
    NSLog(@"width:%ld   height:%ld", (long)a.width, (long)a.height);
    // 结果:width:50   height:60
}

参考:

iOS Masonry链式语法

Masonry源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容