iOS Masonry链式语法

968 阅读8分钟

Demo:github.com/BaiKunlun/c…

Masonry源码:github.com/desandro/ma…

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的源码实现了。 传送门:github.com/desandro/ma… 首先看一下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之间的转换方法,此处不再赘述。

4. 总结

链式语法实际上是将block作为属性访问的一种巧妙运用,当需要进行传参时一定是要通过block来实现的。利用链式语法,我们可以实现很多巧妙的实例调转,而开发者无需关心此时是调用了哪个实例的方法。

头条移动平台部长期招聘:

  • 技术方向:专注APM、热修复、移动中台、Flutter等移动端前沿技术,服务字节跳动所有产品(包括今日头条、抖音等巨无霸产品);
  • 给力的队友:前微信、支付宝、QQ、手百等大牛伴你左右,随时随地交流学习业界最顶尖的黑科技,助你快速成长独当一面;
  • 职业发展:行业最具竞争力的薪酬,快速上升期互联网独角兽,业界公认的顶尖移动端技术团队,来到这里将是你职业生涯中浓墨重彩的一笔;
  • 邮箱:baikunlun@bytedance.com