一、前言
通過本文,你可以了解到 什么是DSL,怎么實現(xiàn)鏈式DSL, 如何去封裝優(yōu)化,以及 輕松使用 UIBezierPath
當然,本文所采用的例子是對
UIBezierPath的封裝,一句代碼就可以畫貝塞爾曲線,全程沒有任何[],你會發(fā)現(xiàn),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是一種基于特定領(lǐng)域的語言,它使工作更貼近于客戶的理解,而不是實現(xiàn)本身,這樣有利于開發(fā)過程中,所有參與人員使用同一種語言進行交流。
而不管做什么開發(fā),總有一個任務就是希望能夠更加簡潔、更加語義化地去表達自己的邏輯,鏈式調(diào)用是一種常見的處理方式。
實際iOS開發(fā)中,相信大家應該都用過,比如說常用來做
界面約束布局的 Masonry ,它的調(diào)用是這樣的,對比系統(tǒng)的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);
}]
三、如何實現(xiàn)鏈式DSL
讀了 Masonry 源碼,你應該能發(fā)現(xiàn),有 兩種方式 能實現(xiàn)鏈式 DSL,同時適用了兩種場合(需不需要外部傳參),其實根本來說就一種方式,就是通過重寫屬性的getter方法,只不過屬性返回值不一樣,對應了不同的場合而已。
** 1、通過屬性的getter方法,返回對象本身,此時不需要外界傳參 **
** 2、通過屬性的getter方法,返回block回調(diào),此時可以通過block傳參 **
個人看法,鏈式語法的結(jié)構(gòu)一般可分三層,
創(chuàng)建對象-中間變量-結(jié)束詞。其中中間變量這部分建議使用中間類來處理,一來體現(xiàn)封裝性,對某一部分的功能應該獨立出來處理,避免單個類代碼冗余;二來調(diào)用更加方便清晰,可讀性更強。本文是針對UIBezierPath的封裝,查看了UIBezierPath的 api ,很容易發(fā)現(xiàn)對應的結(jié)構(gòu)。
- 1、創(chuàng)建對象方法(就是創(chuàng)建
UIBezierPath對象的方法)(部分api舉例)
+ (instancetype)bezierPath;
+ (instancetype)bezierPathWithRect:(CGRect)rect;
- 2、中間變量(就是初始化
UIBezierPath對象的屬性以及方法,不限于系統(tǒng),可自定義)(部分api舉例)
@property(nonatomic) CGFloat lineWidth;
- (void)moveToPoint:(CGPoint)point;
- 3、結(jié)束詞(就是結(jié)束當前的鏈式語法,我們都知道,只有當
UIBezierPath對象調(diào)用stroke或fill才會繪制出來)(部分api舉例)
- (void)fill;
- (void)stroke;
四、鏈式實現(xiàn) UIBezierPath的封裝
為了更加方便大家使用,本次封裝是為
UIBezierPath添加分類,使用更加方便簡單。
-
1、中間類創(chuàng)建,本文中間類命名為
FLBezierPathMaker,繼承自NSObject。定義需要初始化UIBezierPath對象的屬性,將系統(tǒng)的api轉(zhuǎn)換(此時屬性可自定義,不局限于系統(tǒng)的),因為只需要getter方法,因此屬性都是readonly修飾,如果需要參數(shù)傳入,那么就定義block屬性,如果不需要參數(shù)傳入,普通中間類的對象即可。注意:需要鏈式拼接的屬性都需要返回中間類
FLBezierPathMaker對象本身(部分api舉例)
/**
- @author gitKong
- 當前繪制BezierPath的顏色,stroke 對應 線,fill 對應填充
*/
@property (nonatomic,weak,readonly)FLBezierPathMaker (^color)(UIColor color);
/ - @author gitKong
- 起點,可多次設置,對應系統(tǒng)api:@property(nonatomic) CGFloat lineWidth;
/
@property (nonatomic,weak,readonly)FLBezierPathMaker (^lineWidth)(CGFloat lineWidth);
/ - @author gitKong
- 起點,可多次設置,對應系統(tǒng)api:- (void)moveToPoint:(CGPoint)point;
*/
@property (nonatomic,weak,readonly)FLBezierPathMaker *(^moveTo)(CGFloat x,CGFloat y);
- 2、創(chuàng)建對象,參考 [Masonry ](https://github.com/SnapKit/Masonry) 的做法,通過傳入設置中間變量的 `makeOperation` 回調(diào),并實現(xiàn),這個回調(diào)參數(shù)是 `FLBezierPathMaker` 在創(chuàng)建對象的時候傳遞出去。從而實現(xiàn) `bezierPath` 的初始化信息。(部分api舉例)
/**
- @author gitKong
- 回調(diào)通過 maker 初始化中間類信息,對比系統(tǒng)方法:+ (instancetype)bezierPath;
*/
- (instancetype)fl_bezierPath:(void(^)(FLBezierPathMaker maker))makeOperation;
/*
- @author gitKong
- 回調(diào)通過 maker 初始化中間類信息,對比系統(tǒng)方法:+ (instancetype)bezierPathWithRect:(CGRect)rect;
*/
- (instancetype)fl_bezierPathWithRect:(CGRect) rect makeOperation:(void(^)(FLBezierPathMaker *maker))makeOperation;
- 3、結(jié)束詞,通過結(jié)束詞,結(jié)束當前的鏈式語法,并且實現(xiàn)相對于的功能,本文中就是實現(xiàn)繪制貝塞爾曲線,同樣,`stroke` 和 `fill` 的api已經(jīng)在中間類 `FLBezierPathMaker` 中定義。(部分api舉例)
/**
- @author gitKong
- 默認繪制顏色為UIColor.blackColor,對應系統(tǒng)方法:- (void)stroke;- (void)fill;
*/
@property (nonatomic,weak,readonly)UIBezierPath * (^stroke)();
@property (nonatomic,weak,readonly)UIBezierPath * (^fill)();
- 4、調(diào)用,類似 `Masonry ` 的調(diào)用方式,是不是覺得鏈式語法實現(xiàn)很簡單
[UIBezierPath fl_bezierPath:^(FLBezierPathMaker *maker) {
maker.moveTo(0,20).addLineTo(30,50).stroke();
}];
---
# 五、精簡鏈式DSL代碼
> 上文提過,此時調(diào)用還是需要使用 `[]` 創(chuàng)建對象,能不能像 swift 一樣,可以直接去掉 `[]` ,只需要通過 `類名+點語法` 就能實現(xiàn)對象的創(chuàng)建呢
- 要實現(xiàn)通過 `類名+點語法` 創(chuàng)建對象,必須是一個 `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已經(jīng)支持 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現(xiàn)在支持類屬性。 這個特性從Swift開始作為類型屬性,我們把它帶到Objective-C。 互操作就可以很好處理了。 在此示例中,通過添加類標志class,使用屬性語法聲明類屬性someString。 然后,就可以使用點語法訪問此someString屬性。 **注意:類屬性系統(tǒng)不會幫我們實現(xiàn)(setter 和 getter 方法)**。
不會聲明的,可以在查看系統(tǒng)的 `UIColor` 類:你用X-code 8 以上的話,都可以像Swift一樣獲取顏色:`UIColor.redColor` (不過系統(tǒng)沒提示,我的x-code版本: Version 8.2.1 (8C1002))

---
那么現(xiàn)在就很好辦了,創(chuàng)建對象可以直接使用類屬性來實現(xiàn)了返回,而從上文可知,需要提供一個參數(shù) `makeOperation` 回調(diào),那就意味著,我們需要一個block 的類屬性,這個block類屬性返回值是 `UIBezierPath` 對象,參數(shù)只有一個,就是 `makeOperation` 回調(diào)block(`makeOperation` 參數(shù)是`FLBezierPathMaker`對象,不需要返回值),為了方便大家理解,可看下面代碼(h 和 m文件):
/**
@author gitKong
通過添加 class 修飾,就是類屬性
fl_bezierPath:參數(shù):makeOperation(block);返回值:UIBezierPath *
makeOperation(block):參數(shù):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;
// 實現(xiàn)傳遞過來 makeOperation,并傳出參數(shù):FLBezierPathMaker 對象
if (makeOperation) {
makeOperation(mk);
}
// 存儲當前的 FLBezierPathMaker 對象
_maker = mk;
return path;
}
- (UIBezierPath * (^)(void (^)(FLBezierPathMaker )))fl_bezierPath{
/*- @author gitKong
- 類屬性需要調(diào)用者手動實現(xiàn) getter 方法
*/
return ^(void (^makeOperation)(FLBezierPathMaker *)){
// 傳遞參數(shù),返回 UIBezierPath 對象
return FLCreatePathMaker([UIBezierPath bezierPath],makeOperation);
};
}
---
那么現(xiàn)在創(chuàng)建對象就不需要通過 `[]` 實現(xiàn)了,勇敢地點出來吧(x-code 不提示)
UIBezierPath.fl_bezierPath(^(FLBezierPathMaker *maker) {
maker.moveTo(0,20).addLineTo(30,50).stroke();
});
對比一下之前寫的 `[]` ,是不是覺得舒服很多了,雖然現(xiàn)在全部都是點語法了,但是整體代碼看上去不是那么好讀,而且還有一個可優(yōu)化的點,就是屬性名稱太長了(你可以理解為方法名),寫過 swift的都知道,swift 的api 都是很精簡,同時因為x-code不提示,因此 `^(FLBezierPathMaker *maker) {}` 都是要自己手寫上去,這就不友好了,因為要手寫的太多了,舉個例子:(選個參數(shù)最多的)
/**
- @author gitKong
- 繪制圓弧,對應系統(tǒng)方法:+ (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過來填寫,參數(shù)太多,而且參數(shù)還帶個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` 實例,那么我們可以分開這兩個功能,通過兩個屬性分別實現(xiàn):**(如果需要傳遞參數(shù),那么就使用block屬性,注意不要帶入`FLBezierPathMaker ` 參數(shù)就行)**
/**
- @author gitKong
- 對外提供的 中間類對象,設置中間類的對象屬性
/
@property (nonatomic,weak,readonly)FLBezierPathMaker maker;
/ - @author gitKong
- 類屬性,創(chuàng)建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;
}
此時調(diào)用起來就是我所需要的效果了,而且屬性名稱精簡了,需要傳入的參數(shù)也少了,看代碼翻譯成自然語言就是:**創(chuàng)建一個path,讓移動起點到(0,20),添加線條到點(30,50),繪制**。
UIBezierPath.fl_path.maker.moveTo(0,20).addLineTo(30,50).stroke();
對比系統(tǒng)方式調(diào)用:
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 20)];
[path addLineToPoint:CGPointMake(30, 50)];
[path stroke];
---
# 六、總結(jié)
- 1、以上是封裝的思路以及講解鏈式語法是如何實現(xiàn)的,不單是Swift,OC也可以寫得很優(yōu)雅。
- 2、封裝了 `UIBezierPath` 類,調(diào)用起來很方便,特別是如果繪制線條比較多的時候,可讀性更強。
- 3、也許你會有個疑惑,為什么 `stroke` 和 `fill` 設計成一個block,而不是單純一個普通屬性,因為根本不需要傳參!確實可以這樣實現(xiàn),而且能正常使用,但會報警告??(如下圖),意思就是:**不要用.調(diào)用,用[]括號調(diào)用** 如果這樣不就是回到原點么,因此使用block來實現(xiàn),就能避免這種警告。

- 4、本文封裝的 `stroke` 和 `fill` block,能返回當前的 `UIBezierPath` 對象,而系統(tǒng)的 `stroke` 和 `fill` 是返回 `void`,因為考慮到繪制完畢后可能還需要操作path。
- 5、部分方法需要設置角度angle的值,系統(tǒng)默認是以弧度表示,框架內(nèi)部處理了,比如你使用 `addArcWith`,設置 `startAngle` 值為 0,那么角度就是 0 度。
- 6、如果你有什么疑惑或建議,歡迎留言!同時歡迎大家關(guān)注我,喜歡點個like,會不定時更新技術(shù)文章。
- 7、框架支持cocoaPod,輸入:`pod search FLBezierPath` 查詢最新版本 ,對應 [Github 地址](https://github.com/gitkong/FLBezierPath) 如果覺得可以,點個star喔

文章已經(jīng)同步到我的 [個人博客](https://gitkong.github.io)