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源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容