一、前言
通过本文,你可以了解到 什么是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
对象调用stroke
或fill
才会绘制出来)(部分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 ](https://github.com/SnapKit/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、结束词,通过结束词,结束当前的链式语法,并且实现相对于的功能,本文中就是实现绘制贝塞尔曲线,同样,`stroke` 和 `fill` 的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](https://developer.apple.com/videos/play/wwdc2016/405/) 中(文字在 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](http://upload-images.jianshu.io/upload_images/1085031-11051d866a16b157.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
---
那么现在就很好办了,创建对象可以直接使用类属性来实现了返回,而从上文可知,需要提供一个参数 `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、也许你会有个疑惑,为什么 `stroke` 和 `fill` 设计成一个block,而不是单纯一个普通属性,因为根本不需要传参!确实可以这样实现,而且能正常使用,但会报警告⚠️(如下图),意思就是:**不要用.调用,用[]括号调用** 如果这样不就是回到原点么,因此使用block来实现,就能避免这种警告。
![报警告是不是很不爽](http://upload-images.jianshu.io/upload_images/1085031-2610015efc6b880c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
- 4、本文封装的 `stroke` 和 `fill` block,能返回当前的 `UIBezierPath` 对象,而系统的 `stroke` 和 `fill` 是返回 `void`,因为考虑到绘制完毕后可能还需要操作path。
- 5、部分方法需要设置角度angle的值,系统默认是以弧度表示,框架内部处理了,比如你使用 `addArcWith`,设置 `startAngle` 值为 0,那么角度就是 0 度。
- 6、如果你有什么疑惑或建议,欢迎留言!同时欢迎大家关注我,喜欢点个like,会不定时更新技术文章。
- 7、框架支持cocoaPod,输入:`pod search FLBezierPath` 查询最新版本 ,对应 [Github 地址](https://github.com/gitkong/FLBezierPath) 如果觉得可以,点个star喔
![FLBezierPath](http://upload-images.jianshu.io/upload_images/1085031-7dca2528341b3537.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/620)
文章已经同步到我的 [个人博客](https://gitkong.github.io)