VIPER 架构
2014-06-07-viper-intro-0a53d9f8.jpgVIPER单词是
View
,Interactor
,Presenter
,Entity
和Routing
的反义词。Clean Architecture
将应用程序的逻辑结构划分为不同的责任层。这使得隔离依赖关系(例如您的数据库)以及在层之间的边界处测试交互更容易
VIPER的主要部分是:
-
View
:显示演示者告诉的内容,并将用户输入中继回演示者。 -
Interactor
:包含用例指定的业务逻辑。 -
Presenter
:包含视图逻辑,用于准备要显示的内容(从Interactor接收)并响应用户输入(通过从Interactor请求新数据)。 -
Entity
:包含交互器使用的基本模型对象。 -
Routing
:包含导航逻辑,用于描述按什么顺序显示哪些屏幕。
这种分离也符合单一责任原则
, Interactor
对业务分析师负责,Presenter
代表交互设计师,而View
对视觉设计师负责。
Interactor
包含指定的业务逻辑
Interactor
代表应用程序中的单个用例。它包含操纵模型对象Entities(实体)以执行特定任务的业务逻辑。在Interactor
中完成的业务逻辑应独立于任何UI。可以在iOS应用程序或OS X应用
程序中使用相同的Interactor
。
因为Interactor
是普通的PONSO (Plain Old NSObject) ,所以使用TDD进行开发很容易。
Eg:
- (void)findUpcomingItems
{
__weak typeof(self) welf = self;
NSDate* today = [self.clock today];
NSDate* endOfNextWeek = [[NSCalendar currentCalendar] dateForEndOfFollowingWeekWithDate:today];
[self.dataManager todoItemsBetweenStartDate:today endDate:endOfNextWeek completionBlock:^(NSArray* todoItems) {
[welf.output foundUpcomingItems:[welf upcomingItemsFromToDoItems:todoItems]];
}];
}
Entity实体
实体是由Interactor操纵的实体对象。实体仅由Interactor操纵。Interactor从不将实体传递到表示层(即Presenter)。
实体也往往是PONSO。如果您使用的是Core Data,则希望您的托管对象保留在数据层后面。交互器不应与一起使用NSManagedObjects。
这是我们待办事项的实体:
@interface VTDTodoItem : NSObject
@property (nonatomic, strong) NSDate* dueDate;
@property (nonatomic, copy) NSString* name;
+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;
@end
Presenter
Presenter 是一个PONSO,主要是由驱动UI的逻辑组成,它知道何时显示用户界面。它从用户交互中收集输入,以便它可以更新UI并将请求发送到Interactor。当用户点击+按钮添加新的待办事项时,addNewEntry将被调用。对于此操作,演示者要求线框呈现用于添加新项目的UI
- (void)addNewEntry
{
[self.listWireframe presentAddInterface];
}
Presenter还从Interactor接收结果,并将结果转换为有效显示在视图中的形式。
- (void)foundUpcomingItems:(NSArray*)upcomingItems
{
if ([upcomingItems count] == 0)
{
[self.userInterface showNoContentMessage];
}
else
{
[self updateUserInterfaceWithUpcomingItems:upcomingItems];
}
}
实体永远不会从Interactor传递给Presenter。而是将没有行为的简单数据结构从Interactor传递给Presenter。这样可以防止在Presenter中完成任何“实际工作”。演示者只能准备要在视图中显示的数据。
View(视图)
视图是被动的。它等待演示者将其内容显示出来。它从不要求演示者提供数据。为视图定义的方法(例如,用于登录屏幕的LoginView)应允许Presenter进行更高级别的抽象交流,以其内容表示,而不是该内容的显示方式。演示者不知道的存在UILabel,UIButton等等。主持人只知道它维护的内容以及何时应该显示它。由视图决定内容的显示方式。
View是一个抽象接口,在Objective-C中使用协议进行了定义。一个UIViewController或其中一个子类将实现View协议。例如,示例中的“添加”屏幕具有以下界面:
@protocol VTDAddViewInterface <NSObject>
- (void)setEntryName:(NSString *)name;
- (void)setEntryDueDate:(NSDate *)date;
@end
当用户点击“取消”按钮时,视图控制器将告诉该事件处理程序该用户已指示应取消添加操作。这样,事件处理程序可以解决关闭添加视图控制器并告知列表视图进行更新的问题。
The boundary between the View and the Presenter is also a great place for ReactiveCocoa. In this example, the view controller could also provide methods to return signals that represent button actions. This would allow the Presenter
to easily respond to those signals without breaking separation of responsibilities.
View和Presenter之间的边界也是ReactiveCocoa的好地方。在此示例中,视图控制器还可以提供返回表示按钮操作的信号的方法。这将使演示者可以轻松响应这些信号,而不会破坏职责分离。
Routing(路由)
从一个屏幕到另一个屏幕的路线是由交互设计器创建的wireframes定义的。在VIPER中,路由的职责在两个对象(Presenter和wireframes)之间共享。wireframes对象拥有的UIWindow,UINavigationController,UIViewController等,这是负责创建一个视图/视图控制器,并在窗口安装。
由于Presenter包含对用户输入做出反应的逻辑,因此Presenter知道何时导航到另一个屏幕以及导航到哪个屏幕。同时,wireframes知道如何导航。因此,Presenter将使用wireframes执行导航。它们共同描述了从一个屏幕到下一个屏幕的路由。
wireframes也是处理导航过渡动画的明显位置。从添加wireframes查看以下示例:
@implementation VTDAddWireframe
- (void)presentAddInterfaceFromViewController:(UIViewController *)viewController
{
VTDAddViewController *addViewController = [self addViewController];
addViewController.eventHandler = self.addPresenter;
addViewController.modalPresentationStyle = UIModalPresentationCustom;
addViewController.transitioningDelegate = self;
[viewController presentViewController:addViewController animated:YES completion:nil];
self.presentedViewController = viewController;
}
#pragma mark - UIViewControllerTransitioningDelegate Methods
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return [[VTDAddDismissalTransition alloc] init];
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source
{
return [[VTDAddPresentationTransition alloc] init];
}
@end
VIPER配套的应用组件
iOS应用程序体系结构需要考虑以下事实:UIKit和Cocoa Touch是应用程序基于其构建的主要工具。架构需要与应用程序的所有组件和平共处,但是还需要提供有关框架的某些部分如何使用以及它们在哪里居住的指南。
iOS应用的主力军是UIViewController。很容易假设竞争者取代MVC会避免大量使用视图控制器。但是视图控制器是该平台的核心:它们可以处理方向变化,响应用户输入,与导航控制器等系统组件很好地集成在一起,并且现在与iOS 7集成在一起,可以在屏幕之间进行自定义转换。它们非常有用。
使用VIPER,视图控制器可以完全实现其意图:控制视图。我们的待办事项列表应用程序有两个视图控制器,一个用于列表屏幕,一个用于添加屏幕。添加视图控制器的实现非常基础,因为它要做的就是控制视图:
@implementation VTDAddViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(dismiss)];
[self.transitioningBackgroundView addGestureRecognizer:gestureRecognizer];
self.transitioningBackgroundView.userInteractionEnabled = YES;
}
- (void)dismiss
{
[self.eventHandler cancelAddAction];
}
- (void)setEntryName:(NSString *)name
{
self.nameTextField.text = name;
}
- (void)setEntryDueDate:(NSDate *)date
{
[self.datePicker setDate:date];
}
- (IBAction)save:(id)sender
{
[self.eventHandler saveAddActionWithName:self.nameTextField.text
dueDate:self.datePicker.date];
}
- (IBAction)cancel:(id)sender
{
[self.eventHandler cancelAddAction];
}
#pragma mark - UITextFieldDelegate Methods
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[textField resignFirstResponder];
return YES;
}
@end
当应用程序连接到网络时,它们通常更具吸引力。但是,该联网应该在哪里进行,启动它的责任是什么?通常由Interactor来启动网络操作,但是它不会直接处理网络代码。它将询问依赖项,例如网络管理器或API客户端。交互器可能必须聚合来自多个源的数据,以提供实现用例所需的信息。然后,由演示者决定Interactor返回的数据并将其格式化以进行演示。
数据存储负责向交互器提供实体。当Interactor应用其业务逻辑时,它将需要从数据存储中检索实体,操纵实体,然后将更新后的实体放回数据存储中。数据存储区管理实体的持久性。实体不知道数据存储,因此实体不知道如何持久保存自己。
Interactor也不应该知道如何持久化实体。有时,Interactor可能希望使用一种称为数据管理器的对象来促进其与数据存储的交互。数据管理器处理更多特定于商店的操作类型,例如创建访存请求,构建查询等。这使Interactor可以将更多的精力放在应用程序逻辑上,而不必了解有关实体如何收集或持久化的任何信息。当您使用Core Data时,一个使用数据管理器的例子如下所述。
这是示例应用程序的数据管理器的界面:
@interface VTDListDataManager : NSObject
@property (nonatomic, strong) VTDCoreDataStore *dataStore;
- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;
@end
使用TDD开发Interactor时,可以通过测试double / mock切换生产数据存储。不与远程服务器通信(对于Web服务)或触摸磁盘(对于数据库)可以使测试更快,更可重复。
保持数据存储为具有清晰边界的不同层的一个原因是,它允许您延迟选择特定的持久性技术。如果您的数据存储是单个类,则可以使用基本的持久性策略启动应用程序,然后在需要时再升级到SQLite或Core Data,而无需更改应用程序代码库中的任何其他内容。
在iOS项目中使用Core Data通常会引发比架构本身更多的争论。但是,将核心数据与VIPER一起使用可能是您曾经拥有的最佳核心数据体验。核心数据是保持数据同时保持快速访问和低内存占用的绝佳工具。但是它习惯于在NSManagedObjectContext整个应用的实现文件中(尤其是不应该存在的地方)都卷须。VIPER将核心数据保留在应有的位置:在数据存储层。
在待办事项列表示例中,应用程序仅知道正在使用Core Data的两个部分是数据存储库本身(用于设置Core Data堆栈)和数据管理器。数据管理器执行获取请求,将NSManagedObjects数据存储返回的结果转换为标准PONSO模型对象,然后将其传递回业务逻辑层。这样,应用程序的核心永远不会依赖于核心数据,此外,您也不必担心陈旧或处理不佳的线程NSManagedObjects。
发出访问核心数据存储的请求时,数据管理器内部的外观如下:
@implementation VTDListDataManager
- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate*)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock
{
NSCalendar *calendar = [NSCalendar autoupdatingCurrentCalendar];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(date >= %@) AND (date <= %@)", [calendar dateForBeginningOfDay:startDate], [calendar dateForEndOfDay:endDate]];
NSArray *sortDescriptors = @[];
__weak typeof(self) welf = self;
[self.dataStore
fetchEntriesWithPredicate:predicate
sortDescriptors:sortDescriptors
completionBlock:^(NSArray* entries) {
if (completionBlock)
{
completionBlock([welf todoItemsFromDataStoreEntries:entries]);
}
}];
}
- (NSArray*)todoItemsFromDataStoreEntries:(NSArray *)entries
{
return [entries arrayFromObjectsCollectedWithBlock:^id(VTDManagedTodoItem *todo) {
return [VTDTodoItem todoItemWithDueDate:todo.date name:todo.name];
}];
}
@end
UI Storyboard几乎与Core Data一样引起争议。故事板具有许多有用的功能,而完全忽略它们将是一个错误。但是,在利用情节提要板提供的所有功能时,很难实现VIPER的所有目标。
我们倾向于做出的妥协是选择不使用segues。在某些情况下,使用segue是有意义的,但是segue的危险在于它们很难使屏幕之间以及UI和应用程序逻辑之间的分隔保持完整。根据经验,如果需要执行prepareForSegue方法,我们尽量不要使用segues。
否则,情节提要是实现用户界面布局的好方法,尤其是在使用“自动布局”时。我们选择使用情节提要来实现待办事项清单示例的两个屏幕,并使用诸如此类的代码来执行我们自己的导航:
static NSString *ListViewControllerIdentifier = @"VTDListViewController";
@implementation VTDListWireframe
- (void)presentListInterfaceFromWindow:(UIWindow *)window
{
VTDListViewController *listViewController = [self listViewControllerFromStoryboard];
listViewController.eventHandler = self.listPresenter;
self.listPresenter.userInterface = listViewController;
self.listViewController = listViewController;
[self.rootWireframe showRootViewController:listViewController
inWindow:window];
}
- (VTDListViewController *)listViewControllerFromStoryboard
{
UIStoryboard *storyboard = [self mainStoryboard];
VTDListViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:ListViewControllerIdentifier];
return viewController;
}
- (UIStoryboard *)mainStoryboard
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main"
bundle:[NSBundle mainBundle]];
return storyboard;
}
@end
使用VIPER构建模块
通常在使用VIPER时,您会发现一个屏幕或一组屏幕倾向于作为一个模块组合在一起。可以用几种方法来描述模块,但是通常最好将其视为功能。在播客应用中,模块可以是音频播放器或订阅浏览器。在我们的待办事项列表应用程序中,列表和添加屏幕分别构建为单独的模块。
将应用程序设计为一组模块有一些好处。一是模块可以具有非常清晰和定义明确的界面,并且可以独立于其他模块。这使得添加/删除功能或更改界面向用户呈现各种模块的方式变得更加容易。
我们希望在待办事项清单示例中使模块之间的分隔非常清楚,因此我们为add模块定义了两种协议。第一个是模块接口,它定义模块可以执行的操作。第二个是模块委托,它描述了模块的工作。例:
@protocol VTDAddModuleInterface <NSObject>
- (void)cancelAddAction;
- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;
@end
@protocol VTDAddModuleDelegate <NSObject>
- (void)addModuleDidCancelAddAction;
- (void)addModuleDidSaveAddAction;
@end
网友评论