将链式调用的DSL进行到底

实现对 UIBezierPath 的链式调用

Posted by gitKong on February 18, 2017

一、前言

  • 通过本文,你可以了解到 什么是DSL怎么实现链式DSL如何去封装优化,以及 轻松使用 UIBezierPath

  • 当然,本文所采用的例子是对 UIBezierPath 的封装,一句代码就可以画贝塞尔曲线,全程没有任何 [] ,你会发现,Objective-C 原来也可以这样玩,先给效果吧!

恩,我姓孔

UIBezierPath.fl_path.maker.moveTo(90,200).addLineTo(200,200).addLineTo(130,300).addLineTo(130,450).addLineTo(90,380).addLineTo(250,200).addLineTo(250,450).addLineTo(300,450).addLineTo(300,380).lineWidth(7).lineCapStyle(kCGLineCapRound).lineJoinStyle(kCGLineJoinRound).stroke();

二、什么是DSL(Domain Specific Language)

DSL是一种基于特定领域的语言,它使工作更贴近于客户的理解,而不是实现本身,这样有利于开发过程中,所有参与人员使用同一种语言进行交流。

  • 而不管做什么开发,总有一个任务就是希望能够更加简洁、更加语义化地去表达自己的逻辑,链式调用是一种常见的处理方式。

  • 实际iOS开发中,相信大家应该都用过,比如说常用来做 界面约束布局Masonry ,它的调用是这样的,对比系统的 NSLayoutConstraint 约束,确实使用更加方便和易懂,但我觉得还是不够彻底,因为还是有 [],我能不能把这个也去掉,这个后文会分析。

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}]

三、如何实现链式DSL

读了 Masonry 源码,你应该能发现,有 两种方式 能实现链式 DSL,同时适用了两种场合(需不需要外部传参),其实根本来说就一种方式,就是通过重写属性的getter方法,只不过属性返回值不一样,对应了不同的场合而已。

  • ** 1、通过属性的getter方法,返回对象本身,此时不需要外界传参 **

  • ** 2、通过属性的getter方法,返回block回调,此时可以通过block传参 **

个人看法,链式语法的结构一般可分三层,创建对象 - 中间变量 - 结束词 。其中 中间变量 这部分建议使用中间类来处理,一来体现封装性,对某一部分的功能应该独立出来处理,避免单个类代码冗余;二来调用更加方便清晰,可读性更强。本文是针对 UIBezierPath 的封装,查看了 UIBezierPath 的 api ,很容易发现对应的结构。

  • 1、创建对象方法(就是创建 UIBezierPath 对象的方法)(部分api举例)
+ (instancetype)bezierPath;

+ (instancetype)bezierPathWithRect:(CGRect)rect;
  • 2、中间变量(就是初始化 UIBezierPath 对象的属性以及方法,不限于系统,可自定义)(部分api举例)
@property(nonatomic) CGFloat lineWidth;

- (void)moveToPoint:(CGPoint)point;
  • 3、结束词(就是结束当前的链式语法,我们都知道,只有当 UIBezierPath 对象调用 ` strokefill` 才会绘制出来)(部分api举例)
- (void)fill;

- (void)stroke;

四、链式实现 UIBezierPath的封装

为了更加方便大家使用,本次封装是为 UIBezierPath 添加分类,使用更加方便简单。

  • 1、中间类创建,本文中间类命名为 FLBezierPathMaker ,继承自 NSObject。定义需要初始化 UIBezierPath 对象的属性,将系统的api转换(此时属性可自定义,不局限于系统的),因为只需要 getter 方法,因此属性都是 readonly 修饰,如果需要参数传入,那么就定义block属性,如果不需要参数传入,普通中间类的对象即可。

注意:需要链式拼接的属性都需要返回中间类FLBezierPathMaker 对象本身(部分api举例)

/**
*  @author gitKong
*
*  当前绘制BezierPath的颜色,stroke 对应 线,fill 对应填充
*/
@property (nonatomic,weak,readonly)FLBezierPathMaker *(^color)(UIColor *color);
/**
*  @author gitKong
*
*  起点,可多次设置,对应系统api:@property(nonatomic) CGFloat lineWidth;
*/
@property (nonatomic,weak,readonly)FLBezierPathMaker *(^lineWidth)(CGFloat lineWidth);
/**
*  @author gitKong
*
*  起点,可多次设置,对应系统api:- (void)moveToPoint:(CGPoint)point;
*/
@property (nonatomic,weak,readonly)FLBezierPathMaker *(^moveTo)(CGFloat x,CGFloat y);

  • 2、创建对象,参考 Masonry 的做法,通过传入设置中间变量的 makeOperation 回调,并实现,这个回调参数是 FLBezierPathMaker 在创建对象的时候传递出去。从而实现 bezierPath 的初始化信息。(部分api举例)
/**
*  @author gitKong
*
*  回调通过 maker 初始化中间类信息,对比系统方法:+ (instancetype)bezierPath;
*/
+ (instancetype)fl_bezierPath:(void(^)(FLBezierPathMaker *maker))makeOperation;
/**
*  @author gitKong
*
*  回调通过 maker 初始化中间类信息,对比系统方法:+ (instancetype)bezierPathWithRect:(CGRect)rect;
*/
+ (instancetype)fl_bezierPathWithRect:(CGRect) rect makeOperation:(void(^)(FLBezierPathMaker *maker))makeOperation;
  • 3、结束词,通过结束词,结束当前的链式语法,并且实现相对于的功能,本文中就是实现绘制贝塞尔曲线,同样,strokefill 的api已经在中间类 FLBezierPathMaker 中定义。(部分api举例)
/**
*  @author gitKong
*
*  默认绘制颜色为UIColor.blackColor,对应系统方法:- (void)stroke;- (void)fill;
*/
@property (nonatomic,weak,readonly)UIBezierPath * (^stroke)();
@property (nonatomic,weak,readonly)UIBezierPath * (^fill)();
  • 4、调用,类似 Masonry  的调用方式,是不是觉得链式语法实现很简单
[UIBezierPath fl_bezierPath:^(FLBezierPathMaker *maker) {
maker.moveTo(0,20).addLineTo(30,50).stroke();
}];

五、精简链式DSL代码

上文提过,此时调用还是需要使用 [] 创建对象,能不能像 swift 一样,可以直接去掉 [] ,只需要通过 类名+点语法 就能实现对象的创建呢

  • 要实现通过 类名+点语法 创建对象,必须是一个 getter 方法,而且这个 getter 方法是类方法。简单来说,就是需要 类属性getter 方法!

  • 属性就是对象嘛,虽然类属性在 Objective-C 是存在的,因为一切皆对象,但以前从来没有能显式开放使用,但这个东西在 Swift 中是有的,例如:public var characters: String.CharacterView,是 String 的一个类属性。

  • 可喜的是,在 WWDC 2016 What’s New in LLVM 中(文字在 Transcript 中),有提到从 X-code 8开始,LLVM已经支持 Objective-C 显式声明类属性了,这是为了与 Swift 中的类属性互操作而引入的,原文如下:

Objective-C now supports class properties. This feature started in Swift as type properties, and we’ve brought it to Objective-C. Interoperation works great. In this example, the class property someString is declared using property syntax, by adding a class flag. Later, this someString property is accessed using dot syntax. Class properties are never synthesized.

Objective-C现在支持类属性。 这个特性从Swift开始作为类型属性,我们把它带到Objective-C。 互操作就可以很好处理了。 在此示例中,通过添加类标志class,使用属性语法声明类属性someString。 然后,就可以使用点语法访问此someString属性。 注意:类属性系统不会帮我们实现(setter 和 getter 方法)

不会声明的,可以在查看系统的 UIColor 类:你用X-code 8 以上的话,都可以像Swift一样获取颜色:UIColor.redColor (不过系统没提示,我的x-code版本: Version 8.2.1 (8C1002))

截图来自苹果官方API


那么现在就很好办了,创建对象可以直接使用类属性来实现了返回,而从上文可知,需要提供一个参数 makeOperation 回调,那就意味着,我们需要一个block 的类属性,这个block类属性返回值是 UIBezierPath 对象,参数只有一个,就是 makeOperation 回调block(makeOperation 参数是FLBezierPathMaker对象,不需要返回值),为了方便大家理解,可看下面代码(h 和 m文件):

/**
*  @author gitKong
*
*  通过添加 class 修饰,就是类属性

*  fl_bezierPath:参数:makeOperation(block);返回值:UIBezierPath *

*  makeOperation(block):参数:FLBezierPathMaker *;返回值:void

*/
@property (class,nonatomic,weak,readonly)UIBezierPath *(^fl_bezierPath)(void(^makeOperation)(FLBezierPathMaker *));
UIBezierPath * FLCreatePathMaker(UIBezierPath *path,void (^makeOperation)(FLBezierPathMaker *)){
// 初始化 中间类 对象
FLBezierPathMaker *mk = [[FLBezierPathMaker alloc] init];
mk.path = path;
// 实现传递过来 makeOperation,并传出参数:FLBezierPathMaker 对象
if (makeOperation) {
makeOperation(mk);
}
// 存储当前的 FLBezierPathMaker 对象
_maker = mk;
return path;
}

+ (UIBezierPath * (^)(void (^)(FLBezierPathMaker *)))fl_bezierPath{
/**
*  @author gitKong
*
*  类属性需要调用者手动实现 getter 方法
*/
return ^(void (^makeOperation)(FLBezierPathMaker *)){
// 传递参数,返回 UIBezierPath 对象
return FLCreatePathMaker([UIBezierPath bezierPath],makeOperation);
};
}

那么现在创建对象就不需要通过 [] 实现了,勇敢地点出来吧(x-code 不提示)

UIBezierPath.fl_bezierPath(^(FLBezierPathMaker *maker) {
maker.moveTo(0,20).addLineTo(30,50).stroke();
});

对比一下之前写的 [] ,是不是觉得舒服很多了,虽然现在全部都是点语法了,但是整体代码看上去不是那么好读,而且还有一个可优化的点,就是属性名称太长了(你可以理解为方法名),写过 swift的都知道,swift 的api 都是很精简,同时因为x-code不提示,因此 ^(FLBezierPathMaker *maker) {} 都是要自己手写上去,这就不友好了,因为要手写的太多了,举个例子:(选个参数最多的)

/**
*  @author gitKong
*
*  绘制圆弧,对应系统方法:+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
*/
@property (class,nonatomic,weak,readonly)UIBezierPath *(^fl_arcCenter)(CGPoint center,CGFloat radius,CGFloat startAngle,CGFloat endAngle,BOOL clockwise,void(^maker)(FLBezierPathMaker *));
/**
*  @author gitKong
*
*  因为x-code 不提示,我最快的方法只能通过去h文件copy过来填写,参数太多,而且参数还带个block,太麻烦
*/
UIBezierPath.fl_arcCenter(CGPointMake(30, 30),30,0,60,YES,^(FLBezierPathMaker *maker){
maker.moveTo(0,20).addLineTo(30,50).stroke();
});

那么怎么去掉^(FLBezierPathMaker *maker) {} 呢?

  • 首先我们要清楚,这个东西是干什么的,这个是外界传入的一个block,这个block提供一个 FLBezierPathMaker 对象可以让外界设置中间类的属性,同时又能返回当前的 UIBezierPath 实例,那么我们可以分开这两个功能,通过两个属性分别实现:(如果需要传递参数,那么就使用block属性,注意不要带入FLBezierPathMaker 参数就行)
/**
*  @author gitKong
*
*  对外提供的 中间类对象,设置中间类的对象属性
*/
@property (nonatomic,weak,readonly)FLBezierPathMaker *maker;
/**
*  @author gitKong
*
*  类属性,创建UIBezierPath实例
*/
@property (class,nonatomic,weak,readonly)UIBezierPath *fl_path;
static FLBezierPathMaker *_maker = nil;

- (FLBezierPathMaker *)maker{
return _maker;
}

+ (UIBezierPath *)fl_path{
return FLCreatePath([UIBezierPath bezierPath]);
}
UIBezierPath * FLCreatePath(UIBezierPath *path){
FLBezierPathMaker *mk = [[FLBezierPathMaker alloc] init];
mk.path = path;
// 存储当前的FLBezierPathMaker
_maker = mk;
return path;
}

此时调用起来就是我所需要的效果了,而且属性名称精简了,需要传入的参数也少了,看代码翻译成自然语言就是:创建一个path,让移动起点到(0,20),添加线条到点(30,50),绘制

UIBezierPath.fl_path.maker.moveTo(0,20).addLineTo(30,50).stroke();

对比系统方式调用:

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 20)];
[path addLineToPoint:CGPointMake(30, 50)];
[path stroke];

六、总结

  • 1、以上是封装的思路以及讲解链式语法是如何实现的,不单是Swift,OC也可以写得很优雅。

  • 2、封装了 UIBezierPath 类,调用起来很方便,特别是如果绘制线条比较多的时候,可读性更强。

  • 3、也许你会有个疑惑,为什么 strokefill 设计成一个block,而不是单纯一个普通属性,因为根本不需要传参!确实可以这样实现,而且能正常使用,但会报警告⚠️(如下图),意思就是:不要用.调用,用[]括号调用 如果这样不就是回到原点么,因此使用block来实现,就能避免这种警告。

报警告是不是很不爽

  • 4、本文封装的 strokefill block,能返回当前的 UIBezierPath 对象,而系统的 strokefill 是返回 void,因为考虑到绘制完毕后可能还需要操作path。

  • 5、部分方法需要设置角度angle的值,系统默认是以弧度表示,框架内部处理了,比如你使用 addArcWith,设置 startAngle 值为 0,那么角度就是 0 度。

  • 6、如果你有什么疑惑或建议,欢迎留言!同时欢迎大家前往简书关注我,喜欢点个like,会不定时更新技术文章。

  • 7、框架支持cocoaPod,输入:pod search FLBezierPath 查询最新版本 ,对应 Github 地址 如果觉得可以,点个star喔

FLBezierPath