前言:如今iOS的開發(fā)中琉挖,組件化設(shè)計都成為一種標(biāo)配了鳖孤,要是不用上好像就趕不上潮流了。網(wǎng)上相關(guān)的方案分析也多如牛毛图柏。分析比較多的是URLRoute / 蘑菇街的 url-block 和 Protocol-Class / 和 casa 的target-action冠息。
一吼驶、 方案比較
URLRoute 與蘑菇街 url-block 和Protocol-Class 都不得不維護一套 url 或 Protocol 與組件間的映射炼鞠,通過 url 或 Protocol 去尋找組件戈擒。先不說效率問題捂敌,單是編寫和維護這個映射的代碼艾扮,使用者使用必須先注冊,然后才能使用占婉,模板代碼會充滿整個項目泡嘴。其實小的覺得這份映射是完全沒必要的。
casa 的target-action 不用維護映射表逆济,這個方案使用 “Target_ + 組件名字” 作 為key 通過runtime 尋找服務(wù)酌予。但是 hardcode 的地方太多磺箕,宏定義或者const 變量充滿了頭文件。這樣使用起來必須記憶很多const變量抛虫,編碼效率也稍微下降松靡。在調(diào)用組件的時候,cache 了 targetClass建椰。不曉得是否覺得NSClassFromString 方法不夠高效雕欺。
二、小的試把兩套方案結(jié)合起來棉姐,弄一個叫protocol-module模式的組件化
- 約定
oc 語言中充滿了約定屠列。比如 類的初始化方法 initXXX,系統(tǒng)合成的setter getter方法伞矩。更具體的是KVO觀察了一個類的屬性笛洛,被觀察的類變更為一個帶約定前綴的子類
- (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
小的這套方案最主要的是編寫代碼的約定,思路如下圖
2.說明
小的打個比方乃坤。我們把主app稱為用戶苛让,用戶委托商家生產(chǎn)一個組件。用戶把需求和想怎么使用這個組件告訴商家湿诊∮埽“需求”的實現(xiàn)就是組件本身,“怎么使用”對應(yīng)的就是協(xié)議了枫吧。因為協(xié)議是雙方都知曉的浦旱,所以是公開的。這樣的好處是九杂,用戶不管組件是怎么實現(xiàn)的颁湖,我只要按協(xié)議使用就行了。
圖中public protocol 是定義好的組件調(diào)用協(xié)議例隆,基本協(xié)議規(guī)定了組件回調(diào)的 callback甥捺,和服務(wù)體 serverBody,也就是組件提供服務(wù)的 controller镀层。組件moduleA 镰禾、moduleB等為了使用者方便,都會實現(xiàn)一個interface的類唱逢。這個類遵守對應(yīng)的協(xié)議吴侦,類名必須是 “協(xié)議名 + SI”。比如協(xié)議名是ModuleA坞古,那么interface類的類名就是ModuleASI备韧。
1)編寫協(xié)議和組件
基本協(xié)議定義如下
#ifndef MoudleProtocol_ServerInterface
#define MoudleProtocol_ServerInterface @"SI"
#endif
@protocol BaseModule <NSObject>
@required
// server body
@property(nonatomic, weak) __kindof UIViewController *serverBody;
@optional
// callback
@property(nonatomic, copy) void (^callback) (id params);
@end
通過將 BaseModule 協(xié)議子協(xié)議化,定制特定組件調(diào)用協(xié)議痪枫。例如我們要調(diào)用組件moduleA织堂,我們可以定義一個叫ModuleA的協(xié)議叠艳,這個協(xié)議遵守BaseModule協(xié)議
@protocol ModuleA <BaseModule>
@required
// input
@property(nonatomic, strong) NSString *name;
@end
這個協(xié)議很簡單,只有一個屬性name易阳,指定了我們調(diào)用組件時要傳入的參數(shù)是name附较。
定好了協(xié)議,商家生產(chǎn)出組件后潦俺,他還要做一件事就是實現(xiàn)interface接口類ModuleASI
這個類實現(xiàn)如下
@interface MouduleASI : NSObject <ModuleA>
@end
@implementation MouduleASI
@synthesize callback;
@synthesize name;
@synthesize serverBody;
- (UIViewController *)serverBody {
if (!serverBody) {
UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
serverBody = [sb instantiateViewControllerWithIdentifier: @"OhgRacViewController"];
((OhgRacViewController *)serverBody).interface = self;
}
return serverBody;
}
@end
好了拒课,代碼很簡單不是嗎,商家不會抱怨讓他多干活的黑竞。在MouduleASI的.m文件里捕发,商家簡單的返回一個serverBody疏旨。
2)大功告成了嗎很魂?還差一點
組件化最重要的路由忘了。不過放心檐涝,小的這個路由很簡單的遏匆,老規(guī)矩,先看代碼谁榜,路由定義
@interface OHGRouter : NSObject
+ (instancetype) router;
- (id) interfaceForProtocol:(Protocol *) p;
- (id) interfaceForURL:(NSURL *) url;
// for unit test
- (void) assertForMoudleWithProtocol:(Protocol *) p;
- (void) assertForMoudleWithURL:(NSURL *) url;
// navi type for vc
// push present
- (UIViewController *) findVcOfView:(UIView *) view;
@end
同樣簡單的要死幅聘,小的不用說各位看官都基本明白了,不過窃植,還是啰嗦一下...別打臉
- (instancetype) router;這個方法略
- (id) interfaceForProtocol:(Protocol *) p;這個方法是通過協(xié)議找到對應(yīng)的組件接口類帝蒿,實現(xiàn)如下
- (id)interfaceForProtocol:(Protocol *)p {
Class cls = [self _clsForProtocol:p];
return [[cls alloc] init];
}
- (Class) _clsForProtocol:(Protocol *) p {
NSString *clsString = [NSStringFromProtocol(p) stringByAppendingString: MoudleProtocol_ServerInterface];
return NSClassFromString(clsString);
}
好吧,就這么點代碼巷怜,獲取協(xié)議名葛超,拼接上前面public protocol的宏定MoudleProtocol_ServerInterface @"SI",就是我們的接口類名延塑。這就是我們?yōu)樯短崆白隽思s定的原因绣张。(類似蘋果實現(xiàn)的KVO),這個約定好處在于关带,我們完全不用維護 url-block / protocol-class / target-action等在內(nèi)存的映射侥涵,也就不需要注冊組件了。
- (id) interfaceForURL:(NSURL *) url;這個方法可以處理遠端調(diào)用組件宋雏,我們稍后再說
// for unit test
- (void) assertForMoudleWithProtocol:(Protocol *) p;
- (void) assertForMoudleWithURL:(NSURL *) url;
這兩個方法很有意思芜飘,在單元測試的時候,我們?nèi)菀淄ㄟ^協(xié)議和url檢查磨总,組件是否實現(xiàn)了 interface 接口類嗦明,看代碼
- (void)assertForMoudleWithProtocol:(Protocol *)p {
if (![self _clsForProtocol:p]) {
NSString *protocolName = NSStringFromProtocol(p);
NSString *clsName = [protocolName stringByAppendingString: MoudleProtocol_ServerInterface];
NSString *reason = [NSString stringWithFormat: @"找不到協(xié)議 %@ 對應(yīng)的接口類 %@ 的實現(xiàn)", protocolName, clsName];
[self _throwException: reason];
}
}
如果我們找不到接口類,說明組件還沒寫好舍败,拋出異常招狸。
以上敬拓,路由也就寫好了。
3)很快就完成了裙戏,看看怎么使用
在ViewController.m下
#import "ViewController.h"
#import "OHGRouter.h"
@implementation ViewController
- (IBAction)showModlue:(id)sender {
id <ModuleA> obj = [[OHGRouter router] interfaceForProtocol: @protocol(ModuleA)];
// id<ModuleA> obj = [[OHGRouter router] interfaceForURL:[NSURL URLWithString:@"ModuleA://?name=xiaobaitu"]];
obj.name = @"小白兔";
obj.callback = ^(id params) {
NSLog(@"%@", params);
};
[self.navigationController pushViewController: obj.serverBody animated: YES];
}
@end
在showModlue方法里乘凸,小的簡單的傳入?yún)f(xié)議ModuleA,就能調(diào)用組件了累榜,傳入?yún)?shù)name营勤, 實現(xiàn)回調(diào)callback。并且咱基本上沒有hardcode壹罚,所以還有編碼過程編譯器還能給溫馨提示葛作。在整個ViewController.m,咱并沒有導(dǎo)入任何組件相關(guān)的信息猖凛,路由里也沒有赂蠢,很簡單的咱就實現(xiàn)了組件的解耦。這一切就是因為我們一開始就明確了協(xié)議和組件接口的約定辨泳。
4)還沒完
前面說到路由- (id) interfaceForURL:(NSURL ) url;方法虱岂,從使用就可以看出來這事怎么回事了id<ModuleA> obj = [[OHGRouter router] interfaceForURL:[NSURL URLWithString:@"ModuleA://?name=xiaobaitu"]];
只要我們按格式| 協(xié)議名://參數(shù) *| 調(diào)用就大功告成了,例如: ModuleA://?name=xiaobaitu菠红。
好了第岖,完了
附上demo地址:https://github.com/ovcv8/a_simple_router
ps: 針對@inxx 同學(xué)提出的建議,又有一些新的想法试溯,俺打算開本篇的第二篇蔑滓,這里先打個樁??。