第一篇文章对VIPER进行了简单的介绍,这篇文章将从VIPER的源头开始,比较现有的几种VIPER实现,对VIPER进行进一步的职责剖析,并对各类细节实现问题进行挖掘和探讨。最后给出两个完整的VIPER实现,而且提供快速生成VIPER代码的模板。html
Demo和轮子的github地址是:ZIKViper,路由工具:ZIKRouter。有用请点个star~ 注意,Demo须要先用pod install安装一下依赖库。ios
两个实现展现了如下问题的解决方案:git
VIPER架构,最初是2013年在MutualMobile的技术博客上,由Jeff Gilbert 和 Conrad Stoll 提出的。他们的博客网站有过一次迁移,原文地址已经失效,这是迁移后的博文:MEET VIPER: MUTUAL MOBILE’S APPLICATION OF CLEAN ARCHITECTURE FOR IOS APPS。github
这是文章中提出的架构示意图:数据库
Wireframe能够看做是Router的另外一种表达。能够看到,VIPER之间的关系已经很明确了。以后,做者在2014年在objc.io上发表了另外一篇更详细的介绍文章:Architecting iOS Apps with VIPER。swift
在做者的第一篇文章里,阐述了VIPER是在接触到了Uncle Bob的Clean Architecture后,对Clean Architecture的一次实践。所以,VIPER真正的源头应该是Clean Architecture。后端
由Uncle Bob在2011年提出的Clean Architecture
,是一个平台无关的抽象架构。想要详细学习的,能够阅读做者的原文:Clean Architecture,翻译:干净的架构The Clean Architecture。bash
它经过梳理软件中不一样层之间的依赖关系,提出了一个自外向内,单向依赖的架构,以下图所示:网络
越靠近内层,越变得抽象,越接近设计的核心。越靠近外层,越和具体的平台和实现技术相关。内层的部分彻底不知道外层的存在和实现方式,代码只能从外层向内层引用,目的是为了实现层与层之间的隔离。将不一样抽象程度的层进行隔离,作到了把业务规则和具体实现分离开。你能够把外层看做是内层的delegate,外层只能经过内层提供的delegate接口来使用内层。数据结构
表明了这个软件项目的业务规则。由数据实体体现,是一些能够在不一样的程序应用之间共享的数据结构。
表明了本应用所使用的一些业务规则。封装和实现了用到的业务功能,会将各类实体的数据结构转为在用例中传递的实体类,可是和具体的数据库技术或者UI无关。
接口适配层。将用例的规则和具体的实现技术进行抽象地对接,将用例中用到的实体类转为供数据库存储的格式或者供View展现的格式。相似于MVVM中把Model的数据传递给ViewModel供View显示。
右下角表示了接口适配层中不一样模块间的通讯方式。不一样的模块在业务用例中产生关联和数据传递。Input、Output就是Use Case提供给外层的数据流动接口。
库和驱动层,表明了选用的各类具体的实现技术,例如持久层使用SQLite仍是Core Data,网络层使用NSURLSession、NSURLConnection仍是AFNetworking等。
能够看到,Clean Architecture里已经出现了Use Case、Interactor、Presenter等概念,它为VIPER的工程实现提供了设计思想,VIPER将它的设计转化成了具体的实现。VIPER里的各部分正是存在着由外向内的依赖,从外向内表现为:View -> Presenter -> Interactor -> Entity
,Wireframe
严格来讲也是一类特殊的Use Case,用于不一样模块之间通讯,链接了不一样的Presenter
。
必需要记住的是,VIPER架构是根据由外向内的依赖关系来设计的。这句话是指导咱们进行进一步设计和优化的关键。
MutualMobile的那两篇文章虽然已经明确了VIPER各部分之间的职责,而且给出了简单的Demo,可是对Wireframe部分的实现有些争议,解耦作得不够完全,而且对各层之间如何交互还处在最简单的实现上。以后出现了挺多文章来将VIPER进一步细化,不过某些细节的实现上有些差异,在给出我本身的VIPER以前,我将先对这些实现进行一次综合的比较分析,看看他们都使用了哪些技术,遇到了哪些争议点。不一样实现之间已经公认的地方我就再也不单独列出了。
原文地址:Brigade’s Experience Using an MVC Alternative: VIPER architecture for iOS applications。
文章把VIPER的优势总结了一下,提出了这样的架构图:
他们对VIPER的各部分都没有异议,只是对Interactor的实现进行了进一步细化。用一个Data Manager提供给各个Use Case管理Entity,好比获取、存储功能。在Service中调用网络层去获取服务端的数据。
文章中还认为应该由Wireframe负责初始化整个VIPER,生成各部分的类,并设置依赖关系,而且引用另外一个模块的Wireframe,负责跳转到另外一个界面。
和这个实现相似的还有:
针对VIPER须要编写太多初始化代码的麻烦,可使用Xcode自带的Template解决。而不少做者都提到了一个代码生成工具:Generamba。
文章并无对VIPER进行修改,只是进一步细化了。这应该是一个最简单的实现。若是你要实施VIPER,参照这篇文章来没有什么大问题。可是它没有探讨的问题是:
一个对VIPER十分感兴趣的俄国团队,编写了一本关于VIPER的书:The-Book-of-VIPER。而且给出了一个目前网络上实现完成度最高的开源Demo:rambler-it-ios,以及他们用于实施VIPER的库:ViperMcFlurry。
他们整理的VIPER架构图以下:
和其余实现不一样的是,他们把VIPER的初始化和装配工做单独放到了一个Assembly里,Router只作界面跳转的工做。而且把VIPER内不一样部分之间的通讯统一用Input和Output来表示。Input表示外部主动调用模块提供的接口,Output表示模块经过外部实现所要求的接口,将事件传递到外部。
之因此将模块初始化单独放到Assembly里,是由于Router若是负责初始化本模块,会违背单一职责原则。
这个实现的愿景很好,只是在转变为具体实现的时候不够完美,有不少问题尚待解决。具体能够参见Demo。
我对Typhoon这个依赖注入工具不是特别感冒,它使用了十分复杂的run time技术,想要追踪一个对象的注入过程时,会看得晕头转向。并且它没法实现运行时由调用方动态注入,只能实现预约义好的静态注入。也就是不能动态传参。
在Demo中实现了在执行segue时用block来使用-prepareForSegue:sender:
,实现向目的界面传参,实现了动态注入。可是这样就把路由限定在了storyboard的segue技术上,那么对于那些没有使用storyboard的项目应该怎么办呢?Demo并无给出答案。并且-prepareForSegue:sender:
只能向View传参,可是有一些参数是View不该该接触到的,而是应该直接传给Presenter或者Interactor的。
Output
并不能完整描述它的职责,还能够再进一步划分也就是说,他们的方案在设计上是不错的,但在技术上还有不少改进空间。
Uber因为业务愈来愈复杂,旧项目的架构已经没法知足当前的需求,所以在2016年彻底重构了他们的 rider app。他们借鉴VIPER,而且设计出了一个VIPER的变种架构:Riblets。文章地址:ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP。
架构图以下:
数据流向图:
父模块和子模块之间通讯:
这里只列出一些和VIPER有差别的地方:
最大的改变是将Router从Presenter移到了Interactor,改变了模块的主从关系,整个模块的生命周期如今由Interactor来管理。而以前的VIPER模块是依赖于View的生命周期的。这样一来,整个架构就从View驱动变成了业务驱动,或者数据驱动。
关于这个改变,Uber给出了两个缘由:
Uber团队的确颇有想法。在对他们的这个方案进行深刻实践以前,我没法评论这个方案是好是坏,我只在这里提出一些实践中可能会遇到的问题。
关于Uber给出的第一个缘由,这是Uber团队基于协调两个开发团队的状况而作出的选择,若是咱们没有他们这样统一开发的需求,并无必要借鉴。iOS的UIKit是一个视图驱动的框架,很难作到100%数据驱动,在实践中将会遇到许多须要解决的问题,除非有足够的开发时间,不然不要草率地投入其中。是否要使用数据驱动的设计,仍是应该由项目的业务设计来决定。当数据变化大部分是由后端的Service和网络数据引发时,再去考虑数据驱动吧。例如Uber的地图路线由定位模块不断计算,自动更新,就比较适合使用数据驱动。
关于第二个缘由,一个没有View和Presenter的VIPER,就只剩下Router、Interactor、Model,这时这个模块能够看作是一个能够经过Router调用的Service或者Manager,这个Service有本身的状态和生命周期,Service也能够在View销毁后继续完成剩余的业务工做,只要业务须要,能够进行自持有,自释放。并且这个Service最终仍是会表如今某个View上。这么看来,Router的层级已经升高了,成为了整个app内的模块间通讯工具,能够链接任意模块,不只仅是VIPER,所以Router由谁持有,就彻底由模块内部自由管理了。
只是,在iOS中的VIPER里,实际的路由API都是存在于UIViewController上的,Router会直接和View产生引用,把Router放到和View隔离的Interactor里会破坏隔离。并且从Clean架构的分层来看,层级升高后的Router应该是处在Interface Adapter层和Framework & Driver层之间,而Interactor则是在Application Business Rules层,由Interactor来管理其余角色,会破坏了Clean Architecture里的依赖关系。
好比一个没有View的、用于管理语音通话数据的Interactor,收到了通话异常中断的事件,在处理事件时,它不该该经过Router将本身移除,或者结束整个语音通话业务,或者自动调用从新拨号的业务,这样很容易会让不一样的Use Case之间产生耦合,这些都应该由更上层的Service去选择执行,若是有页面跳转的设计,则应该把事件转发给一个存在Presenter层的Parent VIPER模块,由parent来决定是退出通话界面仍是弹窗提示。当一个Interactor没有Presenter和View时,它必定是另外一个VIPER的子模块。这么看来,在没有View时,或许让Service来持有Router才是正确的。
所以,若是真的有把VIPER变成数据驱动的需求,主要仍是源于Uber给出的第一个基于团队统一的理由。
文章里还给出了一些颇有参考价值的内容,好比:
Uber的这个方案讲了不少其余方案没有提到的方面,好比依赖注入、如何引入子模块等问题。不过这个方案并无开源。
首先总结出一个绝对标准的VIPER,各部分遵循隔离关系,同时考虑到依赖注入、子模块通讯、模块间解耦等问题,将VIPER的各部分的职责变得更加明确,也新增了几个角色。示例图以下,各角色的颜色和Clean Architecture图中各层的颜色对应:
示例代码将用一个笔记应用做为演示。
View能够是一个UIView + UIViewController,也能够只是一个custom UIView,也能够是一个自定义的用于管理UIView的Manager,只要它实现了View的接口就能够。
View层的职责:
View层会引入各类自定义控件,这些控件有许多delegate,都在View层实现,统一包装后,再交给Presenter层实现。由于Presenter层并不知道View的实现细节,所以也就不知道这些控件的接口,Presenter层只知道View层统一暴露出来的接口。并且这些控件的接口在定义时可能会将数据获取、事件回调、控件渲染接口混杂起来,最具表明性的就是UITableViewDataSource
里的-tableView:cellForRowAtIndexPath:
。这个接口同时涉及到了UITableViewCell
和渲染cell所须要的Model,是很是容易产生耦合的地方,所以须要作一次分解。应该在View的dataSource里定义一个从外部获取所须要的简单类型数据的方法,在-tableView:cellForRowAtIndexPath:
里用获取到的数据渲染cell。示例代码:
@protocol ZIKNoteListViewEventHandler <NSObject>
- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath;
@end
复制代码
@protocol ZIKNoteListViewDataSource <NSObject>
- (NSInteger)numberOfRowsInSection:(NSInteger)section;
- (NSString *)textOfCellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSString *)detailTextOfCellForRowAtIndexPath:(NSIndexPath *)indexPath;
@end
复制代码
@interface ZIKNoteListViewController () <UITableViewDelegate,UITableViewDataSource>
@property (nonatomic, strong) id<ZIKNoteListViewEventHandler> eventHandler;
@property (nonatomic, strong) id<ZIKNoteListViewDataSource> viewDataSource;
@property (weak, nonatomic) IBOutlet UITableView *noteListTableView;
@end
@implementation ZIKNoteListViewController
- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
text:(NSString *)text
detailText:(NSString *)detailText {
UITableViewCell *cell = [self.noteListTableView dequeueReusableCellWithIdentifier:@"noteListCell" forIndexPath:indexPath];
cell.textLabel.text = text;
cell.detailTextLabel.text = detailText;
return cell;
}
#pragma mark UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.viewDataSource numberOfRowsInSection:section];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *text = [self.viewDataSource textOfCellForRowAtIndexPath:indexPath];
NSString *detailText = [self.viewDataSource detailTextOfCellForRowAtIndexPath:indexPath];
UITableViewCell *cell = [self cellForRowAtIndexPath:indexPath
text:text
detailText:detailText];
return cell;
}
#pragma mark UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[self.eventHandler handleDidSelectRowAtIndexPath:indexPath];
}
@end
复制代码
通常来讲,viewDataSource和eventHandler都是由Presenter来担任的,Presenter接收到dataSource请求时,从Interactor里获取并返回对应的数据。你也能够选择在View和Presenter之间用ViewModel来进行交互。
Presenter由View持有,它的职责有:
Presenter是View和业务之间的中转站,它不包含业务实现代码,而是负责调用现成的各类Use Case,将具体事件转化为具体业务。Presenter里不该该导入UIKit,不然就有可能入侵View层的渲染工做。Presenter里也不该该出现Model类,当数据从Interactor传递到Presenter里时,应该转变为简单的数据结构。
示例代码:
@interface ZIKNoteListViewPresenter () <ZIKNoteListViewDataSource, ZIKNoteListViewEventHandler>
@property (nonatomic, strong) id<ZIKNoteListWireframeProtocol> wireframe;
@property (nonatomic, weak) id<ZIKViperView,ZIKNoteListViewProtocol> view;
@property (nonatomic, strong) id<ZIKNoteListInteractorInput> interactor;
@end
@implementation ZIKNoteListViewPresenter
#pragma mark ZIKNoteListViewDataSource
- (NSInteger)numberOfRowsInSection:(NSInteger)section {
return self.interactor.noteCount;
}
- (NSString *)textOfCellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *title = [self.interactor titleForNoteAtIndex:indexPath.row];
return title;
}
- (NSString *)detailTextOfCellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *content = [self.interactor contentForNoteAtIndex:indexPath.row];
return content;
}
#pragma mark ZIKNoteListViewEventHandler
- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *uuid = [self.interactor noteUUIDAtIndex:indexPath.row];
NSString *title = [self.interactor noteTitleAtIndex:indexPath.row];
NSString *content = [self.interactor noteContentAtIndex:indexPath.row];
[self.wireframe pushEditorViewForEditingNoteWithUUID:uuid title:title content:content delegate:self];
}
@end
复制代码
Ineractor的职责:
Interactor是业务的实现者和维护者,它会调用各类Service来实现业务逻辑,封装成明确的用例。而这些Service在使用时,也都是基于接口的,由于Interactor的实现不和具体的类绑定,而是由Application注入Interactor须要的Service。
示例代码:
@protocol ZIKNoteListInteractorInput <NSObject>
- (void)loadAllNotes;
- (NSInteger)noteCount;
- (NSString *)titleForNoteAtIndex:(NSUInteger)idx;
- (NSString *)contentForNoteAtIndex:(NSUInteger)idx;
- (NSString *)noteUUIDAtIndex:(NSUInteger)idx;
- (NSString *)noteTitleAtIndex:(NSUInteger)idx;
- (NSString *)noteContentAtIndex:(NSUInteger)idx;
@end
复制代码
@interface ZIKNoteListInteractor : NSObject <ZIKNoteListInteractorInput>
@property (nonatomic, weak) id dataSource;
@property (nonatomic, weak) id eventHandler;
@end
@implementation ZIKNoteListInteractor
- (void)loadAllNotes {
[[ZIKNoteDataManager sharedInsatnce] fetchAllNotesWithCompletion:^(NSArray *notes) {
[self.eventHandler didFinishLoadAllNotes];
}];
}
- (NSArray<ZIKNoteModel *> *)noteList {
return [ZIKNoteDataManager sharedInsatnce].noteList;
}
- (NSInteger)noteCount {
return self.noteList.count;
}
- (NSString *)titleForNoteAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] title];
}
- (NSString *)contentForNoteAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] content];
}
- (NSString *)noteUUIDAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] uuid];
}
- (NSString *)noteTitleAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] title];
}
- (NSString *)noteContentAtIndex:(NSUInteger)idx {
if (self.noteList.count - 1 < idx) {
return nil;
}
return [[self.noteList objectAtIndex:idx] content];
}
@end
复制代码
向Interactor提供各类封装好的服务,例如数据库的访问、存储,调用定位功能等。Service由Application在执行路由时注入到Builder里,再由Buidler注入到Interactor里。也能够只注入一个Service Router,在运行时再经过这个Service Router懒加载须要的Service,至关于注入了一个提供Router功能的Service。
Service能够看做是没有View的VIPER,也有本身的路由和Builder。
翻译成中文叫线框,用于表达从一个Module到另外一个Module的过程。虽然也是扮演者执行路由的角色,可是其实它和Router是有区别的。
Wireframe和storyboard中链接好的一个个segue相似,负责提供一系列具体的路由用例,这个用例里已经配置好了源界面和目的界面的一些依赖,包括转场动画、模块间传参等。Wireframe的接口是提供给模块内部使用的,它经过调用Router来执行真正的路由操做。
示例代码:
@interface ZIKTNoteListWireframe : NSObject <ZIKTViperWireframe>
- (void)presentLoginViewWithMessage:(NSString *)message delegate:(id<ZIKTLoginViewDelegate>)delegate completion:(void (^ __nullable)(void))completion;
- (void)dismissLoginView:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
- (void)presentEditorForCreatingNewNoteWithDelegate:(id<ZIKTEditorDelegate>)delegate completion:(void (^ __nullable)(void))completion;
- (void)pushEditorViewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate;
- (UIViewController *)editorViewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate;
- (void)pushEditorViewController:(UIViewController *)destination fromViewController:(UIViewController *)source animated:(BOOL)animated;
- (void)quitEditorViewWithAnimated:(BOOL)animated;
@end
复制代码
Router则是由Application提供的具体路由技术,能够简单封装UIKit里的那些跳转方法,也能够用URL Router来执行路由。可是一个模块是不须要知道app使用的是什么具体技术的。Router才是真正链接各个模块的地方。它也负责寻找对应的目的模块,而且经过Buidler进行依赖注入。
示例代码:
@interface ZIKTRouter : NSObject <ZIKTViperRouter>
///封装UIKit的跳转方法
+ (void)pushViewController:(UIViewController *)destination fromViewController:(UIViewController *)source animated:(BOOL)animated;
+ (void)popViewController:(UIViewController *)viewController animated:(BOOL)animated;
+ (void)presentViewController:(UIViewController *)viewControllerToPresent fromViewController:(UIViewController *)source animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
+ (void)dismissViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
@end
@implementation ZIKTRouter (ZIKTEditor)
+ (UIViewController *)viewForCreatingNoteWithDelegate:(id<ZIKTEditorDelegate>)delegate {
return [ZIKTEditorBuilder viewForCreatingNoteWithDelegate:delegate];
}
+ (UIViewController *)viewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate {
return [ZIKTEditorBuilder viewForEditingNoteWithUUID:uuid title:title content:content delegate:delegate];
}
@end
复制代码
由Application实现,负责在模块通讯时进行一些接口的转换,例如两个模块使用了相同业务功能的某个Service,使用的protocol实现同样,可是protocol名字不同,就能够在路由时,在Adapter里进行一次转换。甚至只要定义的逻辑同样,依赖参数的名字和数据类型也能够容许不一样。这样就能让模块不依赖于某个具体的protocol,而是依赖于protocol实际定义的依赖和接口。
注意这里的Adapter和Clean Architecture里的Interface Adapter
是不同的。这里的Adapter就是字面意义上的接口转换,而Clean Architecture里的Interface Adapter
层更加抽象,是Use Case层与具体实现技术之间的转换,囊括了更多的角色。
负责初始化整个模块,配置VIPER之间的关系,并对外声明模块须要的依赖,让外部执行注入。
一个VIPER模块能够看作是一个独立的组件,能够被单独封装成一个库,被app引用。这时候,app就负责将各个模块链接起来,也就是图中灰色的Application Context
部分。一个模块,确定是存在于一个上下文环境中才能运行起来的。
Wireframe
-> Router
-> Adapter
-> Builder
实现了一个完整的模块间路由,而且实现了模块间的解耦。
其中Wireframe和Builder是分别由引用者模块和被引用模块提供的,是两个模块的出口和入口,而Router和Adapter则是由模块的使用者——Application实现的。
当两个模块之间存在引用关系时,说明存在业务逻辑上的耦合,这种耦合是业务的一部分,是不可能消除的。咱们能作的就是把耦合尽可能交给模块调用者,由Application来提供具体的类,注入到各个模块之中,而模块内部只面向protocol便可。这样的话,被引用模块只要实现了相同的接口,就能够随时替换,甚至接口有一些差别时,只要被引用模块提供了相同功能的接口,也能够经过Adapter来作接口兼容转换,让引用者模块无需作任何修改。
Wireframe至关于插头,Builder至关于插座,而Router和Adapter至关于电路和转接头,将不一样规格的插座和插头链接起来。把这些链接和适配的工做交给Application层,就能让两个模块实现各自独立。
大部分方案都没有讨论子模块存在的状况。在VIPER里如何引入另外一个VIPER模块?多个模块之间如何交互?子模块由谁初始化、由谁管理?
其余几个实现中,只有Uber较为详细地讨论了子模块的问题。在Uber的Riblets架构里,子模块的Router被添加到父模块的Router,模块之间经过delegate和监听的方式进行通讯。这样作会让模块间产生必定的耦合。若是子模块是因为父View使用了一个子View控件而被引入的,那么父Interactor就会在代码里多出一个子Interactor,这样就致使了View的实现方式影响了Interactor的实现。
子模块的来源有:
子View多是一个UIView,也多是一个Child UIViewController。所以子View有可能须要向外部请求数据,也可能独立完成全部任务,不须要依赖父模块。
若是子View能够独立,那在子模块里不会出现和父模块交互的逻辑,只有把一些事件经过Output传递出去的接口。这时只须要把子View的接口封装在父View的接口里便可,父Presenter和父Interactor是不知道父View提供的这几个接口是经过子View实现的。这样父模块就能接收到子模块的事件了,并且可以保持Interactor和Presenter、View之间从低到高的依赖关系。
若是父模块须要调用子模块的某些功能,或者从子模块获取数据,能够选择封装到父View的接口里,不过若是涉及到数据模型,而且不想让数据模型出如今View的接口中,能够把子Interactor做为父Interactor的一个Service,在引入子模块时,经过父Builder注入到父Interactor里,或者根据依赖关系解耦地再完全一点,注入到父Presenter里,让父Presenter再把接口转发给父Interactor。这样子模块和父模块就能经过Service的形式进行通讯了,而这时,父Interactor也不知道这个Service是来自子模块里的。
在这样的设计下,子模块和父模块是不知道彼此的存在的,只是经过接口进行交互。好处是父View若是想要更换为另外一个相同功能的子View控件,就只须要在父View里修改,不会影响Presenter和Interactor。
这个VIPER的设计是经过接口将各个部分组合在一块儿的,一个类须要设置不少依赖,例如Interactor须要依赖许多Service。这就涉及到了两个问题:
在这个方案中,由Builder声明整个模块的依赖,而后在Builder内部为不一样的类设置依赖,外部在注入依赖时,就没必要知道内部是怎么使用这些依赖参数的。一个类若是有必需的依赖参数,能够直接在init方法里体现,对于那些非必需的依赖,能够经过暴露接口来声明。
若是须要动态注入,而不是在模块初始化时就配置全部的依赖,Builder也能够提供动态注入的接口。
若是你须要把一个模块从MVC重构到VIPER,能够先按照这个步骤:
下面就是第一步里在Controller中能够分隔出的职责:
@implementation ViewController
//------View-------
//View的生命周期
#pragma mark View life
//View的配置,包括布局设置
#pragma mark View config
//更新View的接口
#pragma mark Update view
//View须要从model中获取的数据
#pragma mark Request view data source
//监控、接收View的事件
#pragma mark Send view event
//------Presenter-------
//处理View的事件
#pragma mark Handle view event
//界面跳转
#pragma mark Wireframe
//向View提供配置用的数据
#pragma mark Provide view data source
//提供生成model须要的数据
#pragma mark Provide model data source
//处理业务事件,调用业务用例
#pragma mark Handle business event
//------Interactor-------
//监控、接收业务事件
#pragma mark Send business event
//业务用例
#pragma mark Business use case
//获取生成model须要的数据
#pragma mark Request data for model
//维护model
#pragma mark Manage model
@end
复制代码
这里缺乏了View状态管理、业务状态管理等职责,由于这些状态通常都是@property,用pragma mark不能分隔它们,只能在@interface里声明的时候进行隔离。
上面的方案是以最完全的解耦为目标设计的,在实践中,若是真的彻底按照这个设计,代码量的确不小。其实一些地方的耦合并不会引发多大问题,除非你的模块须要封装成通用组件供多个app使用,不然并不须要按照100%的解耦要求来编写。所以接下来我再总结一个稍微简化的方案,总结一下各部分能够在哪些地方出现耦合,哪些耦合不能出现。
在这个方案里,我使用了一个中介者来减小一部分代码,Router就是一个很适合成为中介者的角色。
架构图以下:
UITableViewDataSource
),不用再封装一遍后交给Presenter改变得最多的就是路由部分。View、Presenter和Interactor均可以使用路由来获取一些模块。View能够经过路由获取子View,Presenter能够经过路由获取其余View模块,Interactor能够经过路由获取Service。
在实现时,能够把Wireframe、Router、Builder整合到一块儿,全都放到Router里,Router由模块实现并提供给外部使用。相似于Brigade团队和Rambler&Co团队的实现。可是他们的实现都是直接在Router里引入其余模块的Router,这样会致使依赖混乱,更好的方式是经过一个中间人统一提供其余模块的接口。
我在这里造了个轮子,经过protocol来寻找须要的模块并执行路由,不用直接导入目的模块中的类,而且提供了Adapter的支持,可让多个protocol指向同一个模块。这样就能避免模块间的直接依赖。
示例代码:
///editor模块的依赖声明
@protocol NoteEditorProtocol <NSObject>
@property (nonatomic, weak) id<ZIKEditorDelegate> delegate;
- (void)constructForCreatingNewNote;
- (void)constructForEditingNote:(ZIKNoteModel *)note;
@end
@implementation ZIKNoteListViewPresenter
- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSAssert([[self.view routeSource] isKindOfClass:[UIViewController class]], nil);
//跳转到编辑器界面;经过protocol获取对应的router类,再经过protocol注入依赖
//App能够用Adapter把NoteEditorProtocol和真正的protocol进行匹配和转接
[ZIKViewRouterToModule(NoteEditorProtocol)
performFromSource:[self.view routeSource] //跳转的源界面
configuring:^(ZIKViewRouteConfiguration<NoteEditorProtocol> *config) {
//路由配置
//设置跳转方式,支持全部界面跳转类型
config.routeType = ZIKViewRouteTypePush;
//Router内部负责用获取到的参数初始化editor模块
config.delegate = self;
[config constructForEditingNote:[self.interactor noteAtIndex:indexPath.row]];
config.prepareForRoute = ^(id destination) {
//跳转前配置目的界面
};
config.routeCompletion = ^(id destination) {
//跳转结束处理
};
config.performerErrorHandler = ^(SEL routeAction, NSError * error) {
//跳转失败处理
};
}];
}
@end
复制代码
这个方案依赖于一个统一的中间人,也就是路由工具,在个人实现里就是ZIKRouter。View、Presenter、Interactor均可以使用对应功能的Router获取子模块。而因为ZIKRouter仍然是经过protocol的方式来和子模块进行交互,所以仍然可保持模块间解耦。惟一的耦合就是各部分都引用了ZIKRouter这个工具。若是你想把模块和ZIKRouter的耦合也去除,可让Router也变成面向接口,由外部注入。
针对两个方案,同时写了两个相同功能的Demo,能够比较一下代码上的区别。地址在:ZIKViper。注意,Demo须要先用pod install安装一下依赖库。
项目里也提供了Xcode File Template用于快速生成VIPER代码模板。把.xctemplate
后缀的文件夹拷贝到~/Library/Developer/Xcode/Templates/
目录下,就能够在Xcode的New->File->Template
里选择代码模板快速生成代码。