路由是实现模块间解耦的一个有效工具。若是要进行组件化开发,路由是必不可少的一部分。目前iOS上绝大部分的路由工具都是基于URL匹配的,优缺点都很明显。这篇文章里将会给出一个更加原生和安全的设计,这个设计的特色是:html
若是你想要一个可以充分解耦、类型安全、有依赖注入功能的路由器,那这个就是目前所能找到的最佳方案。git
这个路由工具是为了实践VIPER模式而设计的,目的是为VIPER提供依赖注入功能,不过它也能够用于MVC、MVP、MVVM,没有任何限制。github
工具和Demo地址:ZIKRouter。web
Required Interface
和 Provided Interface
Provided
模块添加Required Interface
adaptative
类型的路由首先,咱们须要梳理清楚,为何咱们须要Router,Router能带来什么好处,解决什么问题?咱们须要一个什么样的Router?swift
没有路由时,界面跳转的代码就很容易产生模块间耦合。设计模式
iOS中执行界面跳转时,用的是UIViewController上提供的跳转方法:安全
[sourceViewController.navigationController pushViewController:destinationViewController animated:YES]; 复制代码
[sourceViewController presentViewController:destinationViewController animated:YES completion:nil]; 复制代码
若是是直接导入destinationViewController的头文件进行引用,就会致使和destinationViewController模块产生耦合。相似的,一个模块引用另外一个模块时也会产生这样的耦合。所以咱们须要一个方式来获取destinationViewController,但又不能对其产生直接引用。bash
这时候就须要路由提供的"寻找模块"的功能。以某种动态的方式获取目的模块。markdown
那么路由是怎么解决模块耦合的呢?在上一篇VIPER讲解里,路由有这几个主要职责:app
经过这几个功能,就能实现模块间的彻底解耦。
路由最重要的功能就是给出一种寻找某个指定模块的方案。这个方案是松耦合的,获取到的模块在另外一端能够随时被另外一个相同功能的模块替换,从而实现两个模块之间的解耦。
寻找模块的实现方式其实只有有限的几种:
这几种方案的优劣将在以后逐一细说。
一个模块A有时候须要使用其余模块的功能,例如最通用的log功能,不一样的app有不一样的log模块,若是模块A对通用性要求很高,log方法就不能在模块A里写死,而是应该经过外部调用。这时这个模块A就依赖于一个log模块了。App在使用模块A的时候,须要知道它的依赖,从而在使用模块A以前,对其注入依赖。
当经过cocoapods这样的包管理工具来配置不一样模块间的依赖时,通常模块之间是强耦合的,模块是一一对应的,当须要替换一个模块时会很麻烦,容易牵一发而动全身。若是是一个单一功能模块,的确须要依赖其余特定的各类库时,那这样作没有问题。可是若是是一个业务模块中引用了另外一个业务模块,就应该尽可能避免互相耦合。由于不一样的业务模块通常是由不一样的人负责,应该避免出现一个业务模块的简单修改(例如调整了方法或者属性的名字)致使引用了它的业务模块也必须修改的状况。
这时候,业务模块就须要在代码里声明本身须要依赖的模块,让app在使用时提供这些模块,从而充分解耦。
示例代码:
@protocol ZIKLoginServiceInput <NSObject>
- (void)loginWithAccount:(NSString *)account
password:(NSString *)password
success:(void(^_Nullable)(void))successHandler
error:(void(^_Nullable)(void))errorHandler;
@end
复制代码
@interface ZIKNoteListViewController ()
//笔记界面须要登陆后才能查看,所以在头文件中声明,让外部在使用的时候设置此属性
@property (nonatomic, strong) id<ZIKLoginServiceInput> loginService;
@end
复制代码
这个声明依赖的工做实际上是模块的Builder的职责。一个界面模块大部分状况下都不止有一个UIViewController,也有其余一些Manager或者Service,而这些角色都是有各自的依赖的,都统一由模块的Builder声明,再在Builder内部设置依赖。不过在上一篇文章的VIPER讲解里,咱们把Builder的职责也放到了Router里,让每一个模块单独提供一个本身的Router。所以在这里,Router是一个离散的设计,而不是一个单例Router掌管全部的路由。这样的好处就是每一个模块能够充分定制和控制本身的路由过程。
能够声明依赖,也就能够同时声明模块的对外接口。这二者很类似,因此再也不重复说明。
执行路由的同时用Builder进行模块构建,构建的时候就对模块内各个角色进行依赖注入。当你调用某个模块的时候,须要的不是某个简单的具体类,而是一个构建完毕的模块中的某个具体类。在使用这个模块前,模块须要作一些初始化的操做,好比VIPER里设置各个角色之间的依赖关系,就是一个初始化操做。所以使用路由去获取某个模块中的类,一定须要经过模块的Builder进行。不少路由工具都缺失了这部分功能。
你能够把依赖注入简单地当作对目的模块传参。在进行界面跳转和使用某个模块时,常常须要设置目的模块的一些参数,例如设置delegate回调。这时候就必须调用一些目的模块的方法,或者传递一些对象。因为每一个模块须要的参数都不同,目前大部分Router都是使用字典包裹参数进行传递。但其实还有更好、更安全的方案,下面将会进行详解。
你也能够把Router、Builder和Dependency Injector分开,不过若是Router是一个离散型的设计,那么都交给各自的Router去作也很合理,同时可以减小代码量,也可以提供细粒度的AOP。
梳理完了路由的职责,如今来比较一下现有的各类Router方案。关于各个方案的具体实现细节我就再也不展开看,能够参考这篇详解的文章:iOS 组件化 —— 路由设计思路分析。
目前绝大多数的Router都是用一串URL来表示须要打开的某个界面,代码上看来大概是这样:
//注册某个URL,和路由处理进行匹配保存 [URLRouter registerURL:@"settings" handler:^(NSDictionary *userInfo) { UIViewController *sourceViewController = userInfo[@"sourceViewController"]; //获取其余参数 id param = userInfo[@"param"]; //获取须要的界面 UIViewController *settingViewController = [[SettingViewController alloc] init]; [sourceViewController.navigationController pushViewController: settingViewController animated:YES]; }]; 复制代码
//调用路由 [URLRouter openURL:@"myapp://noteList/settings?debug=true" userInfo:params completion:^(NSDictionary *info) { }]; 复制代码
传递一串URL就能打开noteList界面的settings界面,用字典包裹须要传递的参数,有时候还会把UIKit的push、present等方法进行简单封装,提供给调用者。
这种方式的优势和缺点都很突出。
这是动态性最高的方案,甚至能够在运行时随时修改路由规则,指向不一样的界面。也能够很轻松地支持多级页面的跳转。
若是你的app是电商类app,须要常常作活动,app内的跳转规则常常变更,那么就很适合使用URL的方案。
URL的方案是最容易跨平台实现的,iOS、Andorid、web、PC都按照URL来进行路由时,也就能够统一管理多端的路由规则,下降多端各自维护和修改的成本,让不懂技术的运营人员也能够简单快速地修改路由。
和上一条同样,这也是一个和业务强相关的优势。若是你有统一多端的业务需求,使用URL也很合适。
iOS中的URL scheme能够跨进程通讯,从app外打开app内的某个指定页面。当app内的页面都能使用URL打开时,也就直接兼容了URL scheme,无需再作额外的工做。
URL Router的设计只适合UI模块,不适合其余功能性模块的组件。功能性模块的调用并不须要如此强的动态特性,除非是有模块热更新的需求,不然一个模块的调用在一个版本里应该老是稳定不变的,即使要进行模块间解耦,也不该该用这种方式。
字符串匹配的方式没法进行编译时检查,当页面配置出错时,只能在运行时才能发现。若是某个开发人员不当心在字符串里加了一个空格,编译时也没法发现。你能够用宏定义来减小这种出错的概率。
没有高效地声明接口的方式,只能从文档里查找,编写时必须仔细对照字符串及其参数类型。
传参经过字典来进行,参数类型没法保证,并且也没法准确地知道所调用的接口须要哪些参数。当目的模块进行了接口升级,修改了参数类型和数量,那全部用到的地方都要一一修改,而且没有编译器的帮助,你没法知道是否遗漏了某些地方。这将会给维护和重构带来极大的成本。
针对这个问题,蘑菇街的选择是用另外一个Router,用protocol来获取目的模块,再进行调用,增长安全性。
这个方案也很容易理解。把以前的字符串匹配改为了protocol匹配,就能获取到一个实现了某个protocol的对象。
开源方案里只看到了BeeHive实现了这样的方式:
id<ZIKLoginServiceInput> loginService = [[BeeHive shareInstance] createService:@protocol(ZIKLoginServiceInput)];
复制代码
再对这个对象调用protocol中的方法,就十分安全了。在重构和修改时,有了编译器的类型检查,效率更高。
Protocol更加符合OC和Swift原生的设计思想,任何模块均可以使用,而不局限于UI模块。
模块A须要用到登陆模块,可是它要怎么才能声明这种依赖关系呢?若是使用Protocol Router,那就只须要在头文件里定义一个属性:
@property (nonatomic, string) id<ZIKLoginServiceInput> *loginService;
复制代码
若是这个依赖是必需依赖,而不是一个可选依赖,那就添加到初始化参数里:
@interface ModuleA ()
- (instancetype)initWithLoginService:(id<ZIKLoginServiceInput>)loginService;
@end
复制代码
问题是,若是这样的依赖不少,那么初始化方法就会变得很长。所以更好的作法是由Builder进行固定的依赖注入,再提供给外部。目前BeeHive并无提供依赖注入的功能。
你能够维护一份protocol和模块的对照表,使用动态的protocol来尝试动态地更改路由规则,也能够在Protocol Router之上封装一层URL Router专门用于动态性的需求。
使用了Protocol Router就须要再额外处理URL Scheme了。不过这样也是正常的,解析URL Scheme原本就应该放到另外一个单独的模块里。
不少谈到这种方案的文章都会指出,和URL Router相比,Protocol Router会致使调用者引用目的模块的protocol,所以会产生"耦合"。我认为这是对"解耦"的错误理解。
要想避免耦合,首先要弄清楚,咱们须要什么程度的解耦。个人定义是:模块A调用了模块B,模块B的接口或者实如今作出简单的修改时,或者模块B被替换为相同功能的模块C时,模块A不须要进行任何修改。这时候就能够认为模块A和模块B是解耦的。
有些时候,表达出两个模块之间的关联是有意义的。
当一个界面A须要展现一个登陆界面时,它可能须要向登陆界面传递一个"提示语"参数,用于在登陆界面显示一串提示。这时候,界面A在调用登陆界面时,是要求登陆界面可以显示这个自定义提示语的,在业务设计中就存在两个模块间的强关联性。这时候,URL Router和Protocol Router没有任何区别,包括下面将要提到的Target-Action
路由方式,都存在耦合,可是Protocol Router经过简单地改善,是能够把这部分耦合去除的。
URL Router:
[URLRouter openURL:@"login" userInfo:@{@"message":@"请登陆查看笔记详情"}]; 复制代码
Protocol Router:
@protocol LoginViewInput <NSObject> @property (nonatomic, copy) NSString *message; @end //获取登陆界面进行设置 UIViewController<LoginViewInput> *loginViewController = [ProtocolRouter destinationForProtocol:@protocol(LoginViewInput)]; loginViewController.message = @"请登陆查看笔记详情"; 复制代码
因为字典传参的缘由,URL Router只不过是把这种接口上的关联隐藏到了字典key里,它在参数字典里使用@"message"
时,就是在隐式地使用LoginViewInput
的接口。
这种业务设计上致使的模块之间互相关联是不可避免的,也是不须要去隐藏的。隐藏了反而会引来麻烦。若是登陆界面的属性名字变了,从NSString *message
改为了NSString *notifyString
,那么URL Router在register的时候也必须修改传参时的代码。若是register是由登陆界面本身执行和处理的,而不是由App Context来处理的,那么此时参数key是固定为@"notifyString"
的,那就会要求全部调用者的传参key也修改成notifyString
,这种修改若是缺乏编译器的帮助会很危险,目前是用宏来减小这种修改致使的工做量。而Protocol Router在修改时就能充分利用编译器进行检查,可以保证100%安全。
所以,URL Router并不能作到解耦,只是隐藏了接口关联而已。一旦遇到了须要修改或者重构的状况,麻烦就出现了,在替换宏的时候,你还必须仔细检查有没有哪里有直接使用字符串的key。只是简单地修更名字仍是可控的,若是是须要增长参数呢?这时候就根本没法检查哪里遗漏了参数传递了。这就是字典传参的坏处。
关于这部分的讨论,也能够参考Peak大佬的文章:iOS组件化方案。
Protocol Router在这种状况下也须要做出修改,可是它能帮助你安全高效地进行重构。并且只要稍加改进,也能够彻底无需修改。解决方法就是把Protocol分离为Required Interface
和Provided Interface
。
Required Interface
和 Provided Interface
模块的接口实际上是有Required Interface
和Provided Interface
的区别的。Required Interface
就是调用者须要用到的接口,Provided Interface
就是实际的被调用者提供的接口。
在UML的组件图中,就很明确地表现出了这二者的概念。下图中的半圆就是Required Interface
,框外的圆圈就是Provided Interface
:
那么如何实施Required Interface
和Provided Interface
?上一篇文章里已经讨论过,应该由App Context在一个adapter里进行接口适配,从而使得调用者能够继续在内部使用Required Interface
,adapter负责把Required Interface
和修改后的Provided Interface
进行适配。
示例代码:
@protocol ModuleARequiredLoginViewInput <NSObject> @property (nonatomic, copy) NSString *message; @end //Module A中的调用代码 UIViewController<ModuleARequiredLoginViewInput> *loginViewController = [ZIKViewRouterToView(LoginViewInput) makeDestination]; loginViewController.message = @"请登陆查看笔记详情"; 复制代码
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
复制代码
//App Context 中的 Adapter,用Objective-C的category或者Swift的extension进行接口适配 @interface LoginViewController (ModuleAAdapte) <ModuleARequiredLoginViewInput> @property (nonatomic, copy) NSString *message; @end @implementation LoginViewController (ModuleAAdapte) - (void)setMessage:(NSString *)message { self.notifyString = message; } - (NSString *)message { return self.notifyString; } @end 复制代码
用category、extension、NSProxy等技术兼容新旧接口,工做所有由模块的使用和装配者App Context
完成。若是LoginViewController
已经有了本身的message
属性,这时候就说明新的登陆模块是不可兼容的,必须有某一方作出修改。固然,接口适配能作的事情是有限的,例如一个接口从同步变成了异步,那么这时候两个模块也是不能兼容的。
所以,若是模块须要进行解耦,那么它的接口在设计的时候就应该十分仔细,尽可能不要在参数中引入太多其余的模块依赖。
只有存在Required Interface
和Provided Interface
概念的设计,才能作到完全的解耦。目前的路由方案都缺失了这一部分。
CTMediator的方案,把对模块的调用封装到Target-Action中,利用了Objective-C的runtime特性,省略了Target-Action的注册和绑定工做,直接经过CTMediator中介者调用目的模块的方法。
@implementation CTMediator (CTMediatorModuleAActions) - (UIViewController *)CTMediator_viewControllerForDetail { UIViewController *viewController = [self performTarget:kCTMediatorTargetA action:kCTMediatorActionNativFetchDetailViewController params:@{@"key":@"value"} shouldCacheTarget:NO ]; if ([viewController isKindOfClass:[UIViewController class]]) { // view controller 交付出去以后,能够由外界选择是push仍是present return viewController; } else { // 这里处理异常场景,具体如何处理取决于产品 return [[UIViewController alloc] init]; } } @end 复制代码
-performTarget:action:params:shouldCacheTarget:
方法经过NSClassFromString
,获取目的模块提供的Target类,再调用Target提供的Action,实现了方法调用:
@implementation CTMediator - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget { NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName]; NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName]; Class targetClass; NSObject *target = self.cachedTarget[targetClassString]; if (target == nil) { targetClass = NSClassFromString(targetClassString); target = [[targetClass alloc] init]; } SEL action = NSSelectorFromString(actionString); if (target == nil) { // 这里是处理无响应请求的地方之一,这个demo作得比较简单,若是没有能够响应的target,就直接return了。实际开发过程当中是能够事先给一个固定的target专门用于在这个时候顶上,而后处理这种请求的 return nil; } if (shouldCacheTarget) { self.cachedTarget[targetClassString] = target; } if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else { // 有可能target是Swift对象 actionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName]; action = NSSelectorFromString(actionString); if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else { // 这里是处理无响应请求的地方,若是无响应,则尝试调用对应target的notFound方法统一处理 SEL action = NSSelectorFromString(@"notFound:"); if ([target respondsToSelector:action]) { return [self safePerformAction:action target:target params:params]; } else { // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程当中,能够用前面提到的固定的target顶上的。 [self.cachedTarget removeObjectForKey:targetClassString]; return nil; } } } } @end 复制代码
Required Interface
和Provided Interface
,所以其实没法实现彻底解耦。和URL Router同样,在目的模块变化时,调用模块也必须作出修改苹果的storyboard其实也有一套路由API,只不过它的局限性很大。在这里简单介绍一下:
@implementation SourceViewController - (void)showLoginViewController { //调用在storyboard中定义好的segue identifier [self performSegueWithIdentifier:@"presentLoginViewController" sender:nil]; } //perform segue时的回调 - (BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(nullable id)sender { return YES; } //配置目的界面 - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { //用[segue destinationViewController]获取目的界面,再对目的界面进行传参 } @end 复制代码
UIStoryboardSegue是和storyboard一块儿使用的,storyboard中定义好了一些界面跳转的参数,好比转场方式(push、present等),在执行路由前,执行路由的UIViewController会收到回调,让调用者配置目的界面的参数。
在storyboard中链接segue,实际上是跳转到一个已经配置好界面的view controller,也就是和View相关的参数,都已经作好了依赖注入。可是自定义的依赖,却仍是须要在代码中注入,因此又给了咱们一个-prepareForSegue:sender:
回调。
我不建议使用segue,由于它会致使强耦合。可是咱们能够借鉴UIStoryboardSegue的sourceViewController、destinationViewController、封装跳转逻辑到segue子类、对页面执行依赖注入等设计。
总结了几个路由工具以后,个人结论是:路由的选择仍是以业务需求为先。当对动态性要求极高、或者须要多平台统一路由,则选择URL Router,其余状况下,我更倾向于使用Protocol Router。和Peak大大的结论一致。
Protocol Router目前并无一个成熟的开源方案。所以我造了个轮子,增长了上面提到的一些需求。
每一个模块都对应一个或者多个router子类,在子类中管理各自的路由过程,包括对象的生成、模块的初始化、路由状态管理、AOP等。路由时,须要使用对应的router子类,而不是一个单例router掌管全部的路由。若是想要避免引用子类带来的耦合,能够用protocol动态获取router子类,或者用父类+泛型在调用者中代替子类。
采用离散式的设计的缘由是想让各个模块对路由拥有充分的控制权。
一个router子类的简单实现以下:
@interface ZIKLoginViewRouter : ZIKViewRouter @end @implementation ZIKLoginViewRouter //app启动时,注册对应的模块和Router //不使用+load和+initialize方法,由于在Swift中已经不适用 + (void)registerRoutableDestination { [self registerView:[ZIKLoginViewController class]]; [self registerViewProtocol:ZIKRoutableProtocol(ZIKLoginViewProtocol)]; } //执行路由时,返回对应的viewController或者UIView - (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration { UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil]; ZIKLoginViewController *destination = [sb instantiateViewControllerWithIdentifier:@"ZIKLoginViewController"]; return destination; } //检查来自storyboard的界面是否须要让外界进行配置 + (BOOL)destinationPrepared:(UIViewController<ZIKLoginViewProtocol> *)destination { if (destination.loginService != nil) { return YES; } return NO; } //初始化工做 - (void)prepareDestination:(UIViewController<ZIKLoginViewProtocol> *)destination configuration:(__kindof ZIKViewRouteConfiguration *)configuration { if (destination.loginService == nil) { //ZIKLoginService也能够用ZIKServiceRouter动态获取 destination.loginService = [ZIKLoginService new]; } } //路由时的AOP回调 + (void)router:(nullable ZIKViewRouter *)router willPerformRouteOnDestination:(id)destination fromSource:(id)source { } + (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source { } + (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source { } + (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source { } @end 复制代码
你甚至能够在不一样状况下返回不一样的destination,而调用者对此彻底不知情。例如一个alertViewRouter为了兼容UIAlertView和UIAlertController,能够在router内部,iOS8以上使用UIAlertController,iOS8如下则使用UIAlertView。
一切路由的控制都在router类内部,不须要模块作出任何额外的修改。
路由的配置信息都存储在configuration里,在调用者执行路由的时候传入。基本的跳转方法以下:
//跳转到Login界面
[ZIKLoginViewRouter
performFromSource:self //界面跳转时的源界面
configuring:^(ZIKViewRouteConfiguration *config) {
//跳转类型,支持push、presentModally、presentAsPopover、performSegue、show、showDetail、addChild、addSubview、custom、getDestination
config.routeType = ZIKViewRouteTypePush;
config.animated = NO;
config.prepareDestination = ^(id<ZIKLoginViewProtocol> destination) {
//跳转前配置界面
};
config.routeCompletion = ^(id<NoteEditorProtocol> destination) {
//跳转成功并结束处理
};
config.performerErrorHandler = ^(ZIKRouteAction routeAction, NSError * error) {
//跳转失败处理,有失败的详细信息
};
}];
复制代码
Configuration只能在初始化block里配置,出了block之后就没法修改。你也能够用一个configuration子类添加更多自定义信息。
若是不须要复杂的配置,也能够只用最简单的跳转:
[ZIKLoginViewRouter performFromSource:self routeType:ZIKViewRouteTypePush];
复制代码
你能够先初始化一个router,再交给其余角色执行路由:
//初始化router self.loginRouter = [[ZIKLoginViewRouter alloc] initWithConfiguring:^(ZIKViewRouteConfiguration * _Nonnull config) { config.source = self; config.routeType = ZIKViewRouteTypePush; }]; //执行路由 if ([self.loginRouter canPerform] == NO) { NSLog(@"此时没法执行路由:%@",self.loginRouter); return; } [self.loginRouter performRouteWithSuccessHandler:^{ NSLog(@"performer: push success"); } performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) { NSLog(@"performer: push failed: %@",error); }]; 复制代码
当你须要消除已经展现的界面,或者销毁一个模块时,能够调用移除路由方法一键移除:
if ([self.loginRouter canRemove] == NO) { NSLog(@"此时没法移除路由:%@", self.loginRouter); return; } [self.loginRouter removeRouteWithSuccessHandler:^{ NSLog(@"performer: pop success"); } performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) { NSLog(@"performer: pop failed,error:%@",error); }]; 复制代码
从而无需再区分调用pop、dismiss、removeFromParentViewController、removeFromSuperview等方法。
咱们不想让外部引用ZIKLoginViewRouter
头文件致使耦合,调用者只须要获取一个符合了ZIKLoginViewProtocol
的view controller,所以只须要根据ZIKLoginViewProtocol
获取到对应的router子类,而后在子类上调用父类ZIKViewRouter
提供的路由方法便可,这样就能够作到隐藏子类。
使用ZIKViewRouterToView
和ZIKViewRouterToModule
宏,便可经过protocol获取到对应的router子类,而且子类返回的destination一定符合ZIKLoginViewProtocol
:
[ZIKViewRouterToView(ZIKLoginViewProtocol)
performFromSource:self
configuring:^(ZIKViewRouteConfiguration *config) {
config.routeType = ZIKViewRouteTypePush;
config.prepareDestination = ^(id<ZIKLoginViewProtocol> destination) {
//跳转前配置界面
};
config.routeCompletion = ^(id<ZIKLoginViewProtocol> destination) {
//跳转成功并结束处理
};
}];
复制代码
这时候ZIKLoginViewProtocol
就至关于LoginView模块的惟一identifier,不能再用到其余view controller上。你能够用多个protocol注册同一个router,用于区分requiredProtocol
和providedProtocol
。
有时候,一些第三方的模块或者系统模块并无提供本身的router,你能够为其封装一个router,此时能够有多个不一样的router管理同一个UIViewController或者UIView类。这些router可能提供了不一样的功能,好比一样是alertRouter,routerA多是用于封装UIAlertController,routerB多是用于兼容UIAlertView和UIAlertController,这时候要如何区分并获取两个不一样的router?
像这种提供了独特功能的router,须要你使用configuration的子类,而后让子类conform对应功能的protocol。因而就能够根据configuration的protocol来获取对应的router:
[ZIKViewRouterToModule(ZIKCompatibleAlertConfigProtocol) performFromSource:self configuring:^(ZIKViewRouteConfiguration<ZIKCompatibleAlertConfigProtocol> * _Nonnull config) { config.routeType = ZIKViewRouteTypeCustom; config.title = @"Compatible Alert"; config.message = @"Test custom route for alert with UIAlertView and UIAlertController"; [config addCancelButtonTitle:@"Cancel" handler:^{ NSLog(@"Tap cancel alert"); }]; [config addOtherButtonTitle:@"Hello" handler:^{ NSLog(@"Tap hello button"); }]; config.routeCompletion = ^(id _Nonnull destination) { NSLog(@"show custom alert complete"); }; }]; 复制代码
若是模块本身提供了router,而且这个router用于依赖注入,不能被其余router替代,能够声明本router为本模块的惟一指定router,当有多个router尝试管理此模块时,启动时就会产生断言错误。
模块的依赖分为固定依赖和运行时参数依赖。
固定依赖就相似于VIPER各角色之间的依赖关系,是一个模块中固定不变的依赖。这种依赖只须要在router内部的-prepareDestination:configuration:
固定配置便可。
运行时依赖就是外部传入的参数,由configuration负责传递,而后一样是在-prepareDestination:configuration:
中,获取configuration并配置destination。你能够用一个configuration子类和router配对,这样就能添加更多自定义信息。
若是依赖参数很简单,也可让router直接对destination进行配置,声明router的destination遵照ZIKLoginViewProtocol
,让调用者在prepareDestination
里设置destination。可是若是依赖涉及到了model对象的传递,而且因为须要隔离View和Model,destination不能接触到这些model对象,这时候仍是须要让configuration传递依赖,在router内部再把model传给负责管理model的角色。
所以,configuration和destination的protocol就负责依赖声明和暴露接口。调用者只须要传入protocol里要求的参数或者调用一些初始化方法便可,至于router内部怎么使用和配置这些依赖,调用者就不用关心了。
声明一个protocol是一个router的config protocol或者view protocol时,只须要让这个protocol继承自ZIKViewConfigRoutable
或者ZIKViewRoutable
便可。这样,全部的依赖声明均可以在头文件里明确表示,没必要再从文档中查找。
一个模块能够直接在内部用ZIKViewRouterToModule
和ZIKViewRouterToView
动态获取router,也能够在头文件里添加一个router属性,让builder注入。
那么一个模块怎么向builder声明本身须要某个特定功能的router呢?答案是父类+泛型。
ZIKRouter支持用泛型指定参数类型。在OC中能够这样使用:
//注意这个示例代码只是用于演示泛型的意思,实际运行时必需要用一个ZIKViewRouter子类才能够 [ZIKViewRouter<UIViewController *,ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *> performFromSource:self configuring:^(ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *config) { config.routeType = ZIKViewRouteTypePerformSegue; config.configureSegue(^(ZIKViewRouteSegueConfiguration *segueConfig) { segueConfig.identifier = @"showLoginViewController"; ); }]; 复制代码
ZIKViewRouter<UIViewController *, ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *>
就是一个指定了泛型的类,尖括号中指定了router的destination和configuration类型。这一串说明至关因而在声明:这是一个destination为UIViewController类型,用ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *
做为执行路由时的configuration的router类。你能够对configuration再添加protocol,代表这个configuration必须遵照指定的protocol。
这时你就能够用父类+泛型来声明一个router类,这个router类的configuration符合特定的config protocol。并且在写的时候Xcode会给你自动补全。这是一种很好的隐藏子类的方式,并且符合原生的语法。
可是因为OC中的类都是Class
类型,所以只能这样声明一个实例属性:
@property (nonatomic, strong) ZIKViewRouter<UIViewController *,ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *> *loginViewRouter;
复制代码
Builder只能注入一个router实例,而不是一个router class。所以在OC里通常不这么使用。
可是在Swift这种类型安全语言中这种模式就能更好地发挥做用了,你能够直接注入一个符合某个泛型的router:
//在Builder中注入alertRouter
swiftSampleViewController.alertRouter = Router.to(RoutableViewModule<ZIKCompatibleAlertConfigProtocol>())
复制代码
class SwiftSampleViewController: UIViewController { //在Builder里注入alertRouterClass后,就能够直接用这个routerClass执行路由 var alertRouter: ViewRouter<Any, ZIKCompatibleAlertConfigProtocol>! @IBAction func testInjectedRouter(_ sender: Any) { self.alertRouter.perform( from: self, configuring: { (config, prepareDestination, prepareModule) in prepareModule({ moduleConfig in //moduleConfig在类型推断时就是ZIKCompatibleAlertConfigProtocol,无需在判断后再强制转换 moduleConfig.title = "Compatible Alert" moduleConfig.message = "Test custom route for alert with UIAlertView and UIAlertController" moduleConfig.addCancelButtonTitle("Cancel", handler: { print("Tap cancel alert") }) moduleConfig.addOtherButtonTitle("Hello", handler: { print("Tap Hello alert") }) }) } } } 复制代码
声明了ViewRouter<Any, ZIKCompatibleAlertConfigProtocol>
的属性后,外部就能够直接注入一个对应的router。能够用这种设计模式来转移、集中获取router的职责。
Router能够在定义的时候限制本身的泛型:
Objective-C:
@interface ZIKCompatibleAlertViewRouter : ZIKViewRouter<UIViewController *, ZIKViewRouteConfiguration<ZIKCompatibleAlertConfigProtocol> *>
@end
复制代码
Swift:
class ZIKCompatibleAlertViewRouter: ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol> {
}
复制代码
这样在传递的时候,就可让编译器检查router是否正确。
上面的演示已经展现了类型安全的处理,由protocol和泛型共同完成了这个类型安全的设计。不过有一些问题还须要特别注意。
使用ZIKViewRouterToModule
和ZIKViewRouterToView
时,会对传入的protocol进行编译检查。保证传入的protocol是可路由的protocol,不能随意滥用。具体用到的方式有些复杂,并且在Objective-C和Swift上使用了两种方式来实现编译检查,具体实现能够看源代码。
Swift的自定义泛型不支持协变,因此使用起来有点奇怪。
let alertRouterClass: ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration>.Type //编译错误 //ZIKCompatibleAlertViewRouter.Type is ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol>.Type alertRouterClass = ZIKCompatibleAlertViewRouter.self 复制代码
Swift的自定义泛型不支持子类型转为父类型,所以把ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol>.Type
赋值给ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration>.Type
类型时就会出现编译错误。奇怪的是反过来逆变反而没有编译错误。而Swift原生的集合类型是支持协变的。从2015年开始就有人提议Swift对自定义泛型加入协变,到如今也没支持。在Objective-C里自定义泛型是能够正常协变的。
所以在swift里,使用了另外一个类来包裹真正的router,而这个类是能够随意指定泛型的。
能够用不一样的protocol获取到相同的router。也就是requiredProtocol
和providedProtocol
只要有声明,均可以获取到同一个router。
首先检查requiredProtocol
和providedProtocol
,肯定两个接口提供的功能是一致的。不然没法兼容。
Provided
模块添加Required Interface
requiredProtocol
是外部的要求目的模块额外兼容的,由App Context在ZIKViewAdapter的子类里进行接口兼容。
@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable> @property (nonatomic, copy) NSString *message; @end //Module A中的调用代码 UIViewController<ModuleARequiredLoginViewInput> *loginViewController = [ZIKViewRouterToView(LoginViewInput) makeDestination]; loginViewController.message = @"请登陆查看笔记详情"; 复制代码
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
复制代码
//ZIKEditorAdapter.h,ZIKViewAdapter子类
@interface ZIKEditorAdapter : ZIKViewRouteAdapter
@end
复制代码
//ZIKEditorAdapter.m //用Objective-C的category、Swift的extension进行接口适配 @interface LoginViewController (ModuleAAdapte) <ModuleARequiredLoginViewInput> @property (nonatomic, copy) NSString *message; @end @implementation LoginViewController (ModuleAAdapte) - (void)setMessage:(NSString *)message { self.notifyString = message; } - (NSString *)message { return self.notifyString; } @end @implementation ZIKEditorAdapter + (void)registerRoutableDestination { //注册NoteListRequiredNoteEditorProtocol和ZIKEditorViewRouter匹配 [ZIKEditorViewRouter registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)]; } @end 复制代码
若是遇到protocol里的一些delegate须要兼容:
@protocol ModuleARequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end
@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<ModuleARequiredLoginViewDelegate> delegate;
@end
复制代码
@protocol LoginViewDelegate <NSObject>
- (void)didLogin;
@end
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<LoginViewDelegate> delegate;
@end
复制代码
这种状况在OC里能够hook-setDelegate:
方法,用NSProxy
来进行消息转发,把LoginViewDelegate
的消息转发为对应的ModuleARequiredLoginViewDelegate
中的消息。
不过Swift里就不适合这么干了,相同方法有不一样参数类型时,能够用一个新的router代替真正的router,在新的router里插入一个中介者,负责转发接口:
@implementation ZIKEditorMediatorViewRouter + (void)registerRoutableDestination { //注册NoteListRequiredNoteEditorProtocol,和新的ZIKEditorMediatorViewRouter配对,而不是目的模块中的ZIKEditorViewRouter //新的ZIKEditorMediatorViewRouter负责调用ZIKEditorViewRouter,插入一个中介者 [self registerView:/* mediator的类*/]; [self registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)]; } - (id)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration { //用ZIKEditorViewRouter获取真正的destination id<ProvidedLoginViewInput> realDestination = [ZIKEditorViewRouter makeDestination]; //获取一个负责转发ProvidedLoginViewInput和ModuleARequiredLoginViewInput的mediator id<ModuleARequiredLoginViewInput> mediator = MediatorForDestination(realDestination); return mediator; } @end 复制代码
通常来讲,并不须要当即把全部的protocol都分离为requiredProtocol
和providedProtocol
。调用模块和目的模块能够暂时共用protocol,或者只是简单地改个名字,在第一次须要替换模块的时候再对protocol进行分离。
ZIKViewRouter
把UIKit中路由相关的方法:
-pushViewController:animated:
-presentViewController:animated:completion:
UIPopoverController
的-presentPopoverFromRect:inView:permittedArrowDirections:animated:
UIPopoverPresentationController
的配置-performSegueWithIdentifier:sender:
-showViewController:sender:
-showDetailViewController:sender:
-addChildViewController:
-addSubview:
全都统一封装,能够用枚举一键切换:
[ZIKViewRouterToView(ZIKLoginViewProtocol)
performFromSource:self routeType::ZIKViewRouteTypePush];
复制代码
对应的枚举值:
ZIKViewRouteTypePush
ZIKViewRouteTypePresentModally
ZIKViewRouteTypePresentAsPopover
ZIKViewRouteTypePerformSegue
ZIKViewRouteTypeShow
ZIKViewRouteTypeShowDetail
ZIKViewRouteTypeAddAsChildViewController
ZIKViewRouteTypeAddAsSubview
ZIKViewRouteTypeCustom
ZIKViewRouteTypeGetDestination
移除路由时,也没必要再判断不一样状况分别调用-popViewControllerAnimated:
、-dismissViewControllerAnimated:completion:
、-dismissPopoverAnimated:
、-removeFromParentViewController
、-removeFromSuperview
等方法。
ZIKViewRouter
会在内部自动调用对应的方法。
adaptative
类型的路由-performSegueWithIdentifier:sender:
、-showViewController:sender:
、-showDetailViewController:sender:
这些adaptative
的路由方法,系统会根据不一样的状况适配UINavigationController
和UISplitViewController
,选择调用push
、present
或者其余方式。直接调用时没法明确知道最终调用的是哪一个方法,也就没法移除界面。
ZIKViewRouter
能够识别这些路由方法在调用后真正执行的路由操做,因此你如今也能够在使用这些方法后移除界面。
ZIKViewRouter
也支持在子类中提供自定义的路由和移除路由方法。只要写好对应的协议便可。
App extension里还有一些特有的跳转方法,好比Watch
扩展里WKInterfaceController
的-pushControllerWithName:context:
和-popController
,Share
扩展里SLComposeServiceViewController
的-pushConfigurationViewController:
和-popConfigurationViewController
。
看了一下extension的种类有十几个,懒得一个个去适配了。并且extension里的界面不会特别复杂,不是特别须要路由工具。若是你须要适配extension,能够本身增长,也能够用ZIKViewRouteTypeCustom
来适配。
ZIKViewRouter
支持storyboard,这也是和其余Router相比更强的地方。毕竟storyboard有时候也是很好用的,当使用了storyboard的项目中途使用router的时候,总不能为了适配router,把全部使用storyboard的界面都重构吧?
适配storyboard的原理是hook了全部UIViewController的-prepareForSegue:sender:
方法,检查destinationViewController是否遵照ZIKRoutableView
协议,若是遵照,就说明是一个由router管理的界面,获取注册的对应router类,生成router实例,对其进行依赖注入。若是destination须要传入动态参数,就会调用sourceViewController的-prepareDestinationFromExternal:configuration:
方法,让sourceViewController传参。若是有多个router类注册了同一个view controller,则取随机的一个router。
你不须要对现有的模块作任何修改,就能够直接兼容。并且原来view controller中的-prepareForSegue:sender:
也能照常使用。
ZIKViewRouter
会在一个界面执行路由和移除路由的时候,对全部注册了此界面的router回调4个方法:
+ (void)router:(nullable ZIKViewRouter *)router willPerformRouteOnDestination:(id)destination fromSource:(id)source { } + (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source { } + (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source { } + (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source { } 复制代码
你能够在这些方法中检查界面是否配置正确。也能够用于AOP记录。
例如,你能够为UIViewController
这个全部view controller的父类注册一个router,这样就能够监控全部的UIViewController
子类的路由事件。
ZIKRouter
会在启动时进行全部router的注册,这样就能检测出router是否有冲突、protocol是否和router正确匹配,保证全部router都能正确工做。当检测到错误时,断言将会失败。
ZIKViewRouter
在执行界面路由时,会检测并报告路由时的错误。例如:
unbalanced transition
错误,会致使-viewWillAppear:
、-viewDidAppear:
、-viewWillDisAppear:
、-viewDidDisappear:
等事件的顺序发生错乱)基本上包含了界面跳转时会发生的大部分错误事件。
ZIKRouter
包含ZIKViewRouter
和ZIKServiceRouter
。ZIKViewRouter
专门用于界面跳转,ZIKServiceRouter
则能够添加任意类进行实例获取。
你能够用ZIKServiceRouter
管理须要的类,而且ZIKServiceRouter
增添了和ZIKViewRouter
相同的动态性和泛型支持。
为了错误检查、支持storyboard和注册,ZIKViewRouter
和ZIKServiceRouter
会在app启动时遍历全部类,进行hook和注册的工做。注册时只是把view class、protocol和router class的地址加入字典,不会对内存有影响。
在release模式下,iPhone6s机型上,测试了5000个UIViewController以及5000个对应的router,遍历全部类而且hook的耗时大约为15ms,注册router的耗时大约为50ms。基本上不会遇到性能问题。
若是你不须要支持storyboard,能够去掉view class和router class配对的注册,去掉之后就没法自动为storyboard里的view controller建立router。至于protocol和router的注册,目前彷佛是没法避免的。
简单来讲,ZIKRouter就是一个用于模块间路由,基于接口进行模块发现和依赖注入的Router。它以原生的语法执行路由,在OC和Swift中都能使用。
项目地址在:ZIKRouter。里面包含了一个demo,用于演示iOS中大部分的界面路由场景,建议在横屏iPad上运行。
最后记得点个star~
Demo截图,控制台的输出就是界面路由时的AOP回调: