Architecting iOS Apps with VIPER(译)

原文链接: Architecting iOS Apps with VIPER

哥们认为,这诶老外很好的诠释了iOS的VIPER架构,所以将此文翻译过来,大家观看,希望大家喜欢。在下一篇文章中我会将公司的VIPER架构分享给大家。

居然又做了件白痴的事儿,翻译完才晓得。之前已经有人已经翻译过了。不过没关系,学习吗。你说我抄袭? NO. 哥们英文虽然不好但是有这个

关于架构,这篇文章写的不错,有兴趣的同学可以看看iOS 架构模式 - 简述 MVC, MVP, MVVM 和 VIPER (译)


文章结构:

What is VIPER?
APPlication Design Based on Use Cases
Main Part of VIPER
  • View
  • Interactor
  • Presenter
  • Entity
  • Routing
APPlication Components Fitting in with VIPER
Using VIPER to Build Modules
Testing with VIPER
Conclusion
Swift Addendum
Structs
Type Safety

在建筑领域,我们塑造了我们的建筑,众所周知,后来我们的建筑塑造了我们。 随着所有程序员最终学习,这也适用于构建软件。

重要的是设计我们的代码,以便每个片段都是容易识别,具有明确的目的,并与其他部分以逻辑方式相配合。 这就是我们所说的软件架构。 良好的架构不是什么使产品成功,但它确实使产品可维护,并帮助保持人们的理智保持它!

在本文中,我们将介绍一种称为VIPER的iOS应用程序架构方法。 VIPER已被用于构建许多大型项目,但为了本文的目的,我们将通过构建一个待办事项列表应用程序向您展示VIPER。 你可以按照GitHub上的示例项目:

What is VIPER?

测试并不总是构建iOS应用程序的主要部分。 当我们开始寻求改进Mutual Mobile的测试实践时,我们发现为iOS应用程序编写测试很困难。 我们决定,如果我们要改进我们测试软件的方式,我们首先需要提出一个更好的方式来构建我们的应用程序。 我们称之为VIPER。

VIPER是一个应用程序的清洁架构的iOS应用程序。 VIPER这个词是View,Interactor,Presenter,Entity和Routing。 清洁架构将应用程序的逻辑结构划分为不同的责任层。 这使得更容易隔离依赖(例如您的数据库)和测试在层之间的边界的交互:

清洁的架构

大多数iOS应用程序使用MVC(模型 - 视图控制器)架构。 使用MVC作为应用程序架构可以引导您思考每个类是模型,视图或控制器。 由于许多应用程序逻辑不属于模型或视图,因此通常最终在控制器中。 这导致称为臃肿视图控制器的问题,其中视图控制器最终做太多。 减少这些大规模的视图控制器并不是iOS开发者寻求提高代码质量的唯一挑战,但它是一个很好的开始。

VIPER的独特层通过为应用逻辑和导航相关代码提供清晰的位置来帮助应对这一挑战。 通过应用VIPER,您将注意到我们的待办事项列表示例中的视图控制器是精简,平均,视图控制机器。 您还会发现视图控制器和所有其他类中的代码很容易理解,更容易测试,因此也更容易维护。

Application Design Based on Use Cases

应用程序通常作为一组用例来实现。 用例也称为接受标准或行为,并描述应用程序意图做什么。 也许列表需要按日期,类型或名称排序。 这是一个用例。 用例是负责业务逻辑的应用程序层。 用例应该独立于它们的用户界面实现。 它们也应当小而明确。 决定如何将复杂的应用程序分解成更小的用例是具有挑战性的,需要练习,但它是一个有用的方式来限制每个问题的范围,你正在写的每个类。

使用VIPER构建应用程序涉及实现一组组件以满足每个用例。 应用程序逻辑是实现用例的主要部分,但它不是唯一的部分。 用例也会影响用户界面。 此外,考虑如何将应用程序与应用程序的其他核心组件(如网络和数据持久性)相结合很重要。 组件像插件一样用于用例,VIPER是描述每个组件的作用以及它们如何相互交互的一种方式。

我们的待办事项列表应用程序的用例或要求之一是基于用户的选择以不同的方式将待办事项分组。 通过将组织数据的逻辑分离为用例,我们能够保持用户界面代码干净,并且可以轻松地在测试中包装用例,以确保它继续按照我们的预期方式工作。

Main Parts of VIPER

The main parts of VIPER are:

  • View: displays what it is told to by the Presenter and relays user input back to the Presenter.

(视图:显示Presenter告知的内容,并将用户输入中继回Presenter。)

  • Interactor: contains the business logic as specified by a use case.

(交互器:包含用例指定的业务逻辑。)

  • Presenter: contains view logic for preparing content for display (as received from the Interactor) and for reacting to user inputs (by requesting new data from the Interactor).

(表示层,也可称主持人:包含用于准备显示内容(如从Interactor接收的)和用于对用户输入做出反应(通过从Interactor请求新数据)的视图逻辑。)

  • Entity: contains basic model objects used by the Interactor.

(实体:包含Interactor使用的基本模型对象。)

  • Routing: contains navigation logic for describing which screens are shown in which order.

(路由:包含用于描述按哪个顺序显示哪些屏幕的导航逻辑。)

这种分离也符合单一责任原则。 Interactor负责业务分析师,Presenter代表交互设计师,而View负责视觉设计师。

下面是不同组件及其连接方式的图表:

虽然VIPER的组件可以按任何顺序在应用程序中实现,但我们选择按照我们建议实施它们的顺序介绍组件。
您会注意到,此顺序与构建整个应用程序的过程大致一致,首先讨论产品需要做什么,然后讨论用户如何与之交互。

Interactor (交互器)

交互器表示应用程序中的单个用例。 它包含了操作模型对象(Entities)来执行特定任务的业务逻辑。 在Interactor中完成的工作应该独立于任何UI。 相同的Interactor可以在iOS应用程序或OS X应用程序中使用。

因为交互器是主要包含逻辑的PONSO(Plain Old NSObject),所以使用TDD很容易开发。

示例应用的主要用例是向用户显示任何即将到期的待完成项目(即下周末到期的任何项目)。 此用例的业务逻辑是找到在今天和下周末之间到期的任何待完成项目,并分配相对到期日:今天,明天,本周晚些或下周。

下面是从VTDListInteractor对应的方法:

- (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 (实体)

实体是由交互器操作的模型对象。 实体仅由交互器操纵。 交互器从不将实体传递到表示层(即Presenter)。

实体也倾向于PONSO。 如果使用Core Data,您将希望受管对象保留在数据层之后。 Interactors不应该与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被调用。 对于此操作,Presenter要求线框(wireframe)呈现用于添加新项目的UI:

- (void)addNewEntry
{
    [self.listWireframe presentAddInterface];
}

Presenter还从Interactor接收结果,并将结果转换为在View中有效显示的窗体。

下面是从Interactor接收即将到来的项目的方法。 它将处理数据并确定向用户显示什么:

- (void)foundUpcomingItems:(NSArray*)upcomingItems
{
    if ([upcomingItems count] == 0)
    {
        [self.userInterface showNoContentMessage];
    }
    else
    {
        [self updateUserInterfaceWithUpcomingItems:upcomingItems];
    }
}

Entities从不从Interactor传递给Presenter。 相反,没有行为的简单数据结构从Interactor传递给Presenter。 这防止在Presenter中完成任何“真正的工作”。 Presenter只能准备要在View中显示的数据。

View (View / ViewController)

视图是被动的。 它等待Presenter给它显示内容; 它从不要求Presenter获取数据。 为View定义的方法(例如登录屏幕的LoginView)应允许Presenter以更高的抽象级别进行通信,以其内容表示,而不是如何显示该内容。 Presenter不知道UILabel,UIButton等的存在。Presenter只知道它维护的内容和什么时候应该显示。 由视图决定内容的显示方式。

View是一个抽象的接口,在Objective-C中用协议定义。 UIViewController或其子类之一将实现View协议。 例如,我们的示例中的“添加”屏幕具有以下界面:

@protocol VTDAddViewInterface <NSObject>

- (void)setEntryName:(NSString *)name;
- (void)setEntryDueDate:(NSDate *)date;

@end

当用户点击取消按钮时,视图控制器告诉此事件处理程序用户已经指示它应该取消添加操作。 这样,事件处理程序可以处理解除添加视图控制器并告知列表视图进行更新。

View和Presenter之间的边界也是ReactiveCocoa的一个好地方。 在该示例中,视图控制器还可以提供用于返回表示按钮动作的信号的方法。 这将允许演示者容易地响应这些信号而不破坏责任的分离。

Routing (路由)

从一个屏幕到另一个屏幕的路由在交互设计器创建的wireframes中定义。 在VIPER中,路由的职责由两个对象共享:Presenter和Wireframes。 wireframes对象拥有UIWindow,UINavigationController,UIViewController等。它负责创建一个View / ViewController并在窗口中安装它。

由于Presenter包含对用户输入做出反应的逻辑,因此Presenter知道何时导航到另一个屏幕,以及导航到哪个屏幕。 同时,wireframe知道如何导航。 因此,Presenter将使用wireframe来执行导航。 它们一起描述从一个屏幕到下一个屏幕的路线。

wireframe也是处理导航转换动画的一个明显的地方。 看看这个例子从添加wireframe:

@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

应用程序使用自定义视图控制器转换来呈现添加视图控制器。 由于wireframe负责执行转换,它成为添加视图控制器的转换委托,并且可以返回适当的转换动画。

Application Components Fitting in with VIPER (应用组件与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客户端。 交互器可能必须聚合来自多个源的数据以提供满足用例所需的信息。 然后由Presenter接收由Interactor返回的数据并将其格式化以用于呈现。

数据存储器负责向交互器提供实体。 当交互器应用其业务逻辑时,它将需要从数据存储中检索实体,操纵实体,然后将更新的实体放回数据存储中。 数据存储管理实体的持久性。 实体不知道数据存储,所以实体不知道如何坚持自己。

Interactor不应该知道如何持久化实体。 有时Interactor可能想使用一种称为数据管理器的对象来促进它与数据存储的交互。 数据管理器处理更多的存储特定类型的操作,例如创建获取请求,构建查询等。这允许交互器更多地关注应用逻辑,并且不必知道如何收集或持久化实体。 使用数据管理器有意义的一个例子是当您使用Core Data时,如下所述。

Here’s the interface for the example app’s data manager:
以下是范例应用程式资料管理员的介面:

@interface VTDListDataManager : NSObject

@property (nonatomic, strong) VTDCoreDataStore *dataStore;

- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;

@end

当使用TDD开发交互器时,可以使用测试双/模拟切换生产数据存储。 不与远程服务器通信(对于Web服务)或触摸磁盘(对于数据库)允许您的测试更快,更可重复。

保持数据存储作为具有清晰边界的不同层的一个原因是它允许您延迟选择特定持久性技术。 如果您的数据存储是单个类,您可以使用基本持久性策略启动应用程序,然后稍后升级到SQLite或Core Data,这样做是有意义的,而不需要更改应用程序代码库中的任何其他内容。

在iOS项目中使用Core Data通常比架构本身引发更多的争论。 然而,使用Core Data与VIPER可以是你曾经有过的最好的Core Data体验。 Core Data是一个伟大的工具,用于持久化数据,同时保持快速访问和低内存占用。 但是它有一个习惯,在所有的应用程序的实现文件,特别是他们不应该在它的NSManagedObjectContext卷曲。 VIPER将Core Data保存在应该保存的位置:在数据存储层。

在待办事项列表示例中(In the to-do list example,),应用程序只知道Core Data正在使用的两个部分是数据存储本身,它设置了Core Data堆栈和数据管理器。 (data manager)数据管理器执行提取请求,将数据存储返回的NSManagedObject转换为标准PONSO模型对象,并将它们传递回业务逻辑层。 这样,应用程序的核心从不依赖于Core Data,并且作为一个奖励,你从来不必担心过时或不良线程的NSManagedObjects gunking的工作。

以下是当请求访问Core Data存储时在数据管理器(data manager)中显示的内容:

@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

几乎与Core Data一样有争议的UI Storyboards(故事版)。 Storyboards有许多有用的功能,完全忽略它们将是一个错误。 然而,使用Storyboards提供的所有功能很难实现VIPER的所有目标。

我们倾向于做出的妥协是选择不使用segue。 在某些情况下,使用segue是有意义的,但segue的危险是它们使得保持屏幕之间的分离以及UI和应用逻辑之间的分离是完整的。 根据经验,如果实现prepareForSegue方法是必要的,我们尽量不使用segue。

否则,storyboard是一个伟大的方式来实现您的用户界面的布局,特别是在使用自动布局。 我们选择使用Storyboards实现待办事项列表示例的两个屏幕,并使用此类代码执行自己的导航:

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

Using VIPER to Build Modules

通常当使用VIPER时,你会发现一个屏幕或一组屏幕往往作为一个模块在一起。 模块可以用几种方式描述,但通常最好被认为是一个特性。 在播客应用中,模块可能是音频播放器或订阅浏览器。 在我们的待办事项列表应用程序中(to-do list app),列表和添加屏幕都是作为单独的模块构建的。

将应用程序设计为一组模块有几个好处。 一个是模块可以具有非常清楚和定义明确的接口,以及独立于其他模块。 这使得添加/删除功能更容易,或者更改您的界面向用户呈现各种模块的方式。

我们想在待办事项列表示例中使模块之间的分离非常清楚,因此我们为添加模块定义了两个协议。 第一个是模块接口,它定义模块可以做什么。 第二个是模块委托,它描述了模块做了什么。 例:

@protocol VTDAddModuleInterface <NSObject>

- (void)cancelAddAction;
- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;

@end


@protocol VTDAddModuleDelegate <NSObject>

- (void)addModuleDidCancelAddAction;
- (void)addModuleDidSaveAddAction;

@end

由于模块必须呈现给用户很有价值,所以模块的Presenter通常实现模块接口。 当另一个模块想要呈现这个模块时,它的Presenter将实现模块委托协议,以便它知道模块在呈现时做了什么。

(module)模块可以包括可以用于多个屏幕的实体,交互器和管理器的公共应用逻辑层。 这当然要取决于这些屏幕之间的相互作用以及它们之间的相似性。 模块可以很容易地仅表示单个屏幕,如待办事项列表示例中所示。 在这种情况下,应用逻辑层可以非常特定于其特定模块的行为。

模块也只是一个很好的简单的组织代码的方式。 保持一个模块的所有代码在自己的文件夹中,并在Xcode中的组,使得它很容易找到,当你需要改变的东西。 这是一个伟大的感觉,当你找到一个类,你希望找到它。

使用VIPER构建模块的另一个好处是它们变得更容易扩展到多个形状因子。 为在Interactor层隔离的所有用例提供应用程序逻辑,您可以专注于为平板电脑,手机或Mac构建新的用户界面,同时重用您的应用程序层。

更进一步,iPad应用程序的用户界面可能能够重用iPhone应用程序的一些视图,视图控制器和演示者。 在这种情况下,iPad屏幕将由“超级”演示者和线框表示,这将使用为iPhone编写的现有演示者和线框构成屏幕。 在多个平台上构建和维护应用程序可能相当具有挑战性,但是促进在模型和应用程序层之间重用的良好架构有助于简化这一过程。

Testing with VIPER (用VIPER测试)

以下VIPER鼓励分离关注点,使其更容易采用TDD。 Interactor包含独立于任何UI的纯逻辑,这使得它易于使用测试驱动。 Presenter包含准备显示数据的逻辑,并且独立于任何UIKit窗口小部件。 开发这个逻辑也很容易用测试驱动。

我们首选的方法是从Interactor开始。 UI中的一切都是为了满足用例的需要。 通过使用TDD测试驱动Interactor的API,您将更好地了解UI与用例之间的关系。

例如,我们将看看负责即将到来的待办事项列表的Interactor。 查找即将到来的项目的政策是找到下周末到期的所有待完成项目,并将每个待完成项目分类为今天,明天,本周或下周。

我们写的第一个测试是确保Interactor找到下周末到期的所有待完成项目:

- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek
{
    [[self.dataManager expect] todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY];
    [self.interactor findUpcomingItems];
}

一旦我们知道Interactor要求适当的待办事项,我们将写几个测试来确认它将待办事项分配到正确的相对日期组(例如今天,明天等):

- (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday
{
    NSArray *todoItems = @[[VTDTodoItem todoItemWithDueDate:self.today name:@"Item 1"]];
    [self dataStoreWillReturnToDoItems:todoItems];

    NSArray *upcomingItems = @[[VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationToday dueDate:self.today title:@"Item 1"]];
    [self expectUpcomingItems:upcomingItems];

    [self.interactor findUpcomingItems];
}

现在我们知道了Interactor的API是什么样的,我们可以开发Presenter。 当Presenter从Interactor接收到即将到来的待完成项目时,我们将测试我们是否正确地格式化数据并在UI中显示它:

- (void)testFoundZeroUpcomingItemsDisplaysNoContentMessage
{
    [[self.ui expect] showNoContentMessage];

    [self.presenter foundUpcomingItems:@[]];
}

- (void)testFoundUpcomingItemForTodayDisplaysUpcomingDataWithNoDay
{
    VTDUpcomingDisplayData *displayData = [self displayDataWithSectionName:@"Today"
                                                          sectionImageName:@"check"
                                                                 itemTitle:@"Get a haircut"
                                                                itemDueDay:@""];
    [[self.ui expect] showUpcomingDisplayData:displayData];

    NSCalendar *calendar = [NSCalendar gregorianCalendar];
    NSDate *dueDate = [calendar dateWithYear:2014 month:5 day:29];
    VTDUpcomingItem *haircut = [VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationToday dueDate:dueDate title:@"Get a haircut"];

    [self.presenter foundUpcomingItems:@[haircut]];
}

- (void)testFoundUpcomingItemForTomorrowDisplaysUpcomingDataWithDay
{
    VTDUpcomingDisplayData *displayData = [self displayDataWithSectionName:@"Tomorrow"
                                                          sectionImageName:@"alarm"
                                                                 itemTitle:@"Buy groceries"
                                                                itemDueDay:@"Thursday"];
    [[self.ui expect] showUpcomingDisplayData:displayData];

    NSCalendar *calendar = [NSCalendar gregorianCalendar];
    NSDate *dueDate = [calendar dateWithYear:2014 month:5 day:29];
    VTDUpcomingItem *groceries = [VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationTomorrow dueDate:dueDate title:@"Buy groceries"];

    [self.presenter foundUpcomingItems:@[groceries]];
}

我们还想测试当用户想要添加新的待办事项时,应用程序将启动适当的操作:

- (void)testAddNewToDoItemActionPresentsAddToDoUI
{
    [[self.wireframe expect] presentAddInterface];

    [self.presenter addNewEntry];
}

我们现在可以开发视图。 当没有即将到来的待办事项时,我们要显示一条特殊消息:

- (void)testShowingNoContentMessageShowsNoContentView
{
    [self.view showNoContentMessage];

    XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");
}

当有即将显示的待办事项时,我们要确保表格正在显示:

- (void)testShowingUpcomingItemsShowsTableView
{
    [self.view showUpcomingDisplayData:nil];

    XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");
}

首先构建交互器是与TDD的自然契合。 如果您首先开发Interactor,然后是Presenter,那么您将首先构建一套围绕这些层的测试,并为实现这些用例打下基础。 您可以对这些类进行快速迭代,因为您不必与UI进行交互以测试它们。 然后,当你去开发视图时,你将有一个工作和测试的逻辑和表示层连接到它。 当你完成开发视图时,你可能会发现,第一次运行应用程序一切正常,因为所有通过的测试告诉你,它将工作。

Conclusion (结论)

我们希望你喜欢这个介绍VIPER。 你们很多人现在可能想知道下一步去哪里。 如果你想使用VIPER构建你的下一个应用程序,你从哪里开始?

本文和我们使用VIPER的应用程序示例实现具体和明确定义,因为我们可以使它们。 我们的待办事项列表应用程序(to-do list app)是相当直接,但它也应准确解释如何使用VIPER构建一个应用程序。 在现实世界的项目中,你如何密切关注这个例子将取决于你自己的一套挑战和约束。 根据我们的经验,我们的每个项目都略微改变了使用VIPER的方法,但所有的项目都从使用它来指导他们的方法受益匪浅。

在某些情况下,您希望出于各种原因偏离VIPER设定的路径。 也许你碰到了一个'bunny’objects的战争,或者你的应用程序将受益于在Storyboard中使用segue。 没关系。 在这些情况下,请考虑VIPER在作出决定时代表的精神。 在其核心,VIPER是基于单一责任原则的架构。 如果你有麻烦,在决定如何前进时考虑这个原则。

您可能还想知道是否可以在您的现有应用程序中使用VIPER。 在这种情况下,考虑使用VIPER构建新功能。 我们许多现有的项目都采取了这条路线。 这允许您使用VIPER构建模块,并且还帮助您发现任何现有问题,这可能使得更难采用基于单一责任原则的架构。

开发软件的一个伟大的事情是每个应用程序是不同的,也有不同的方式来架构任何应用程序。 对我们来说,这意味着每个应用程序都是一个学习和尝试新事物的新机会。 如果你决定尝试VIPER,我们认为你会学到一些新的东西。 谢谢阅读。

Swift Addendum (Swift附录)

上周在WWDC苹果推出了Swift编程语言作为Cocoa和Cocoa Touch开发的未来。 对于Swift语言形成复杂的意见还为时过早,但我们知道语言对我们如何设计和构建软件有重大影响。 我们决定重写我们的VIPER TODO示例应用程序使用Swift帮助我们了解这对VIPER意味着什么。 到目前为止,我们喜欢我们所看到的。 这里有一些Swift的功能,我们认为将提高使用VIPER构建应用程序的体验。

Structs

在VIPER中,我们使用小的,轻量级的模型类在层之间传递数据,例如从Presenter到View。 这些PONSO通常旨在简单地携带小量的数据,并且通常不打算被子类化。 Swift结构体非常适合这些情况。 下面是一个在VIPER Swift示例中使用的结构的示例。 注意,这个结构需要是等价的,所以我们重载了==运算符来比较它的类型的两个实例:

struct UpcomingDisplayItem : Equatable, Printable {
    let title : String = ""
    let dueDate : String = ""

    var description : String { get {
        return "\(title) -- \(dueDate)"
    }}

    init(title: String, dueDate: String) {
        self.title = title
        self.dueDate = dueDate
    }
}

func == (leftSide: UpcomingDisplayItem, rightSide: UpcomingDisplayItem) -> Bool {
    var hasEqualSections = false
    hasEqualSections = rightSide.title == leftSide.title

    if hasEqualSections == false {
        return false
    }

    hasEqualSections = rightSide.dueDate == rightSide.dueDate

    return hasEqualSections
}

Type Safety

也许Objective-C和Swift之间的最大的区别是如何处理类型。 Objective-C是动态类型,Swift是非常有意的严格,如何在编译时实现类型检查。 对于像VIPER这样的架构,其中应用程序由多个不同的层组成,类型安全性可以是程序员效率和架构结构的巨大胜利。 编译器帮助您确保容器和对象在层边界之间传递时具有正确的类型。 这是一个使用如上所示的structs的好地方。 如果一个结构体意图生活在两层之间的边界,那么你可以保证它永远不能从这些层之间逃脱由于类型安全。


建议: 与原文一同观看,如果您觉得哪里需要修正。请在下方评论。谢谢观看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容

  • 建筑领域流行这样一句话,“我们虽然在营造建筑,但建筑也会重新塑造我们”。正如所有开发者最终领悟到的,这句话同样适用...
    Answer55阅读 738评论 0 3
  • 第一篇文章对VIPER进行了简单的介绍,这篇文章将从VIPER的源头开始,比较现有的几种VIPER实现,对VIPE...
    黑超熊猫zuik阅读 12,708评论 11 76
  • 前言 在软件开发中,架构是至关重要的一部分,就好比盖房子需要基本的钢筋石桩等骨架,常听到的架构有MVC、MVP、M...
    Tangentw阅读 5,946评论 8 26
  • 前言 看了下上篇博客的发表时间到这篇博客,竟然过了11个月,罪过,罪过。这一年时间也是够折腾的,年初离职跳槽到鹅厂...
    西木柚子阅读 21,201评论 12 185
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,562评论 18 139