一、前言
-
通过本文,你可以了解到 什么是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 的做法,通过传入设置中间变量的
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 中(文字在 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))
那么现在就很好办了,创建对象可以直接使用类属性来实现了返回,而从上文可知,需要提供一个参数 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来实现,就能避免这种警告。
-
4、本文封装的
stroke
和fill
block,能返回当前的UIBezierPath
对象,而系统的stroke
和fill
是返回void
,因为考虑到绘制完毕后可能还需要操作path。 -
5、部分方法需要设置角度angle的值,系统默认是以弧度表示,框架内部处理了,比如你使用
addArcWith
,设置startAngle
值为 0,那么角度就是 0 度。 -
6、如果你有什么疑惑或建议,欢迎留言!同时欢迎大家前往简书关注我,喜欢点个like,会不定时更新技术文章。
-
7、框架支持cocoaPod,输入:
pod search FLBezierPath
查询最新版本 ,对应 Github 地址 如果觉得可以,点个star喔