用 VIPER 构建 iOS 应用架构

http://blog.oneapm.com/apm-tech/101.html

【编者按】本篇文章由 Jeff Gilbert 和 Conrad Stoll 共同编写,通过构建一个基础示例应用,深入了解 VIPER,并从视图、交互器等多个部件理清 VIPER 的整体布局及思路。通过 VIPER 构建 iOS 应用架构,提升应用质量,迎接应用构建的新机遇!本文系 OneAPM 工程师编译整理

众所周知,在建筑领域,我们塑造自己的建筑,而建筑也反过来影响我们。对于程序员来说,在构建软件方面这个道理也同样适用。

在编程的过程中,让代码具备可读性是非常重要的,除此之外代码还要具备明确的目的、在逻辑方面能和其他代码协调一致。这就是我们常说的软件架构。好的架构不能保证产品成功,但它却会使产品便于维护,不至于让读到的人抓狂。

在这篇文章中,作者将介绍一种在 iOS 应用中适用的方法,名为 VIPER。 VIPER 已经被用来构建许多大型项目,但这篇文章的目的,是通过建立一个待办事项的应用来深入了解 VIPER。你可以在 GitHub 上找到示例项目。

什么是VIPER?

测试并不总是开发 iOS 应用的重要组成部分。当作者在 Mutual Mobile 寻求提高测试实践的办法时,发现为 iOS 应用程序编写测试并不容易。作者意识到,如果要找到提高测试软件的方法,首先需要想出更好的应用架构。于是把这更好的方法称作 VIPER。

VIPER 是 iOS 程序的整洁架构。它是指 View、Interactor、Presenter、Entity 和 Routing。整洁的体系结构将应用程序的逻辑分配到不同的责任区。这使得依赖关系(例如数据库)更容易独立,更便于测试层与层之间的相互作用。

大多数 iOS 应用正在使用 MVC(模型—视图—控制器)架构。使用 MVC 作为一个程序的体系结构,可以引导你思考各个类是一个模型、视图或控制器。由于大部分应用程序逻辑不属于模型或视图,而是通常在控制器中结束。这便导致了所谓的大规模视图控制器,其视图控制器最终变得繁复巨大。为这些庞大的视图控制器减负是 iOS 开发者必须面对的问题,也是提高代码质量的巨大挑战。

通过定位程序逻辑和导航相关的代码,VIPER 的不同层可以帮助解决这个难题。随着 VIPER 的应用,「待办事项」列表实例中,你会发现视图控制器变得纤小、匀称。视图控制器的代码和所有类都容易理解和测试,更有利于后期维护。

基于用例的应用设计

应用程序常常实现为一组用例。用例被称为验收标准或行为,它同时描述了程序目的。一个列表可能需要按日期、类型或名称进行排序,这就是一个用例。用例是程序的逻辑责任层,应独立于用户接口实现,它们应该是小巧而明确的。决定如何将一个复杂程序拆分成较小的用例是具有挑战性的,同时需要积累实践经验。但它能有效地限制各个问题和类的范围。

用 VIPER 构建应用程序,需要实现一系列组件来满足每个用例。应用逻辑是实现用例的重要且非唯一的组成部分。用例也会影响用户界面。此外,需要考虑用例怎样结合核心部件,比如网络和数据持久性。在用例中,组建就像插件,VIPER 用来描述组件功能,以及它们之间彼此交互的方式。

待办事项应用程序的一个用例或要求就是基于用户选择,对待办事项进行分组。通过将数据转换成用例的逻辑进行分离,我们能够保持用户接口代码的整洁性,方便在测试中包装用例,来确保它以正常方式继续工作。

VIPER 的主要组件

VIPER 的主要组件有以下部分:

  • 视图:显示展示器的要求,并返回用户输入。
  • 交互器:包含用例指定的业务逻辑。
  • 展示器:包含视图逻辑用于准备显示内容(从交互器接收的)并反馈用户输入(通过显示器请求最新数据)。
  • 实体:包含交互器所用的基本模型对象。
  • 路由:包含导航逻辑来描述屏幕出现的顺序。

这种分离也符合单一责任原则。交互器担任业务分析师,展示器则成了交互设计师,视图负责可视化设计。

下面是不同组件的示意图以及它们的相互联系:

虽然 VIPER 的组件在应用中可以以任意顺序组合实现,这里我们选择以推荐的实现顺序来介绍组件。你会发现,这个顺序与应用的构建过程基本一致,首先讨论应用产品需要做什么,其次是用户如何与它进行交互。

交互器

交互代表应用程序中的一个用例,它包含业务逻辑用来操纵模型对象(实体)以进行特定任务。交互器所做的工作应该是独立于任何用户界面的。同样的交互器可以在 iOS 应用或 OS X 应用中使用。

因为交互器是一个 PONSO(普通老式 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]];
    }];
}

实体

实体是由交互器操纵的模型对象(仅由交互器操控),交互器不会将实体传递到表现层(即展示器)。

实体往往也是 PONSOs。如果你正在使用核心数据,你会希望你的管理对象最好保持在数据层后端。交互器不能直接使用 NSManagedObjects。

这是示例应用的实体:

@interface VTDTodoItem : NSObject

@property (nonatomic, strong)   NSDate*     dueDate;
@property (nonatomic, copy)     NSString*   name;

+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;

@end

如果你的实体都只是数据结构也别太惊讶,任何依赖于程序的逻辑,都应该在交互器中。

展示器

展示器是一个 PONSO,主要由逻辑组成来驱动用户界面。它知道何时呈现用户界面,收集用户交互过程的输入,用于实时更新 UI,并像交互器发送响应请求。

当用户点击+按钮来添加新的待办事项,addNewEntry 被调用。为了响应操作,展示器调用线框来显示添加一个新项目的 UI:

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

展示器还可以显示交互器接收结果,并将结果转换成其它能在视图中有效展示的形式。

下面是从展示器接收到待办事项后所调用的方法。它将处理相关数据,并确定将哪些内容展现给用户:

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

实体从来不会从交互器传输到展示器。相反,那些无行为的简单数据结构却可以传输。这样可以防止任何「实际工作」在展示器中进行。展示器只负责准备视图显示中的数据。

视图

视图通常是被动的。它显示展示器传输来的内容;却不能向展示器主动请求数据。为一个视图定义的方法(例如 LoginView 需要登录界面),应该允许展示器在更高的抽象级进行通信,展示器直接展示其内容,而不关心该内容要如何显示。展示器不知道 UILabel、UIButton 等控件,只知道维护内容以及显示时机。内容要如何展示完全取决于视图。

视图是一个抽象的接口,适用协议用 Objective-C 中定义。一个 UIViewController 或它的子类将实现 View 协议。例如本例中的「添加」界面有如下接口:

@protocol VTDAddViewInterface 

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

@end

视图和视图控制器还处理用户交互和用户输入。所以不难理解为什么视图控制器通常很庞大,因为他们最容易处理用户输入并执行相关动作。为了保持视图控制器倾斜,需要让它们在用户采取某些动作后,通知有效途径告知有关各方。视图控制器不对用户动作做出响应,只将事件传递给响应方法。

本例中,添加视图控制器的事件处理属性,有如下接口:

@protocol VTDAddModuleInterface 

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

@end

当用户点击取消按钮,视图控制器告知事件处理机制,用户需要取消此添加操作。这样一来,该事件处理机制可以取消添加视图控制器,并告知列表视图以更新。

视图和展示器之间的边界可用于 ReactiveCocoa。在本例中,视图控制器还能提供方法以返回表示按钮动作的信号。这将允许展示器更容易地对信号做出反馈,而无需破坏责任区域的独立。

路由

界面之间的路由在交互设计师创建的线框中定义。在 VIPER 中,路由的任务是实现展示器和线框之间的共享。线框对象包括 theUIWindow、UINavigationController 和 UIViewController 等,它负责创建视图/视图控制器,并在窗口中完成装配。

由于展示器包含响应用户输入的逻辑,所以它知道何时该导航到其他屏幕,应导航到哪个界面,同时,线框知道如何进行导航。展示器主要使用线框实现导航功能。线框和展示器协同描述一个屏幕到下一个的路由的过程。

线框便于处理导航过渡动画。来看看下面添加线框的例子:

@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)animationControllerForDismissedController:(UIViewController *)dismissed 
{
    return [[VTDAddDismissalTransition alloc] init];
}

- (id)animationControllerForPresentedController:(UIViewController *)presented
                                                                  presentingController:(UIViewController *)presenting
                                                                      sourceController:(UIViewController *)source 
{
    return [[VTDAddPresentationTransition alloc] init];
}

@end

该应用程序使用自定义视图控制器过渡来添加视图控制器。由于线框负责执行过渡,它成为添加视图控制器的过渡委托,并能返回恰当的过渡动画。

用 VIPER 组织应用组件

建造 iOS 应用的架构时需要明白,作为主要开发工具,UIKit 和 Cocaa Touch 的作用是打造应用的「门面」。架构需要与应用的所有组件和平共处,但它也需要为部分框架的使用,以及处于什么位置提供建议。

iOS 应用的主力是 UIViewController,它很时常被认为是取代 MVC 的竞争者,能大量减少使用视图控制器。但是视图控制器是平台的中心:他们处理方向变化、响应用户输入、集成系统组件比如导航控制器。


UIViewController 的确相当有用。

在 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

当应用连上网络才真正的闪耀夺人。然而,应该在什么时候连网呢?哪些来负责启动网络呢?通常情况下,交互器会发起网络连接,但它不会直接处理网络代码,而是会寻找依赖项,比如网络管理员或 API 客户。交互器可以聚集来自多个源的数据,提供实现用例的所需信息。然后就看显示器采集交互器反馈的数据,并格式化用于显示。

数据存储器负责为交互器提供实体。当交互器应用其业务逻辑时,它将从数据存储器中检索实体、操纵实体,然后将更新的实体返回数据存储器。数据存储可以管理实体的持久性,但实体却不知道数据存储,因此更不知道如何坚持自身的持久性。

交互器也不知道如何将实体持久化。有时交互器可能使用名为数据管理器的对象类型,以促进与数据存储器的交互。数据管理器处理多个操作的特定存储类型,如创建提取请求、建立查询等。这使得交互器更专注于应用程序的逻辑,而无需知道实体如何聚集或持续。下面的例子就是说明数据管理器的意义。

这是示例应用的数据管理器接口:

@interface VTDListDataManager : NSObject

@property (nonatomic, strong) VTDCoreDataStore *dataStore;

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

@end

当使用 TDD 开发交互器时,能切换出生产带测试双/模拟的数据存储器。避免远程服务器(Web服务)或触摸盘(数据库)可以使测试更快速,增强其复用性。

保持数据存储作为有明确界限的独立层的原因之一,在于它可以让你推迟选择一个特定的持久化技术。如果你的数据存储器是一个单独的类,你可以用基本的持久化策略来搭建应用,以后待需要时再升级到 SQLite 或核心数据,而不需要对应用代码库进行任何改变。

在 iOS 的项目中使用核心数据往往能激发比架构本身更大的争议。但是,在 VIPER 中使用核心数据可能是最好的核心数据体验。在持久化数据方面,核心数据是保持快速存取和低内存占用的绝佳工具。但它有个缺陷:itsNSManagedObjectContext 像触须似的贯穿所有应用的执行文件,特别是在一些它们不应该出现的地方。 VIPER 则可以保持核心数据出现在正确的地方——数据存储层。

在待办事项示例中,仅有应用程序的两个部件知道核心数据正在使用,其一是数据存储本身,其中建立核心数据堆栈;其二则是数据管理器。数据管理器执行读取请求,将数据存储所返回的 theNSManagedObjects,转换成标准 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

像核心数据一样引起争议的是用户界面故事板。故事板有许多不容忽视的功能。然而,同时采用故事板的所有功能也难以实现 VIPER 的所有目标。

因此,我们往往退一步选择不使用 segues。在某些情况下,使用 segues 是很有意义的,但伴随着 segues 的风险,是难以原封不动地保持界面的独立,以及用户界面和应用程序逻辑之间的分离。一般来说,如果必须实施 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 时,你会发现单个或多个界面往往形成一个模块。模块可以从多个方面进行描述,但最好的是把它当作一种功能。在播客应用中,一个模块可能是音频播放器或订阅浏览器。在我们的待办事项应用中,列表和添加界面均构建成单独模块。

将应用设计为多个模块组合有很多优势。其中之一是,模块具有非常清晰和明确定义的接口,能独立于其他模块。这使得它更容易实现添加或删除功能,也更方便在界面中向用户展示各种模块。

笔者想让待办事项示例中的模块分离得更明确,因此为添加模块定义了两个协议。其一是模块接口,它定义模块可以做什么;其二是模块代理,用来描述模块做了什么。代码如下:

@protocol VTDAddModuleInterface 

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

@end


@protocol VTDAddModuleDelegate 

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

@end

由于模块必须展现出来才有价值,所以模块的展示器通常实现了模块接口。当其他模块想展示当前模块时,它的展示器将实现模块代理协议,因此它知道模块之前显示时做了什么。

一个模块可能包括实体、交互器、管理器,可以被用于多个界面的共同应用逻辑层。当然,这取决于界面之间的交互,以及它们是否类似。模块可以很容易地在待办事项示例中展示单个界面。这样说来,应用逻辑层可以针对特定模块的行为而定制。

模块也是组织代码的简易途径。将模块的所有代码都放在自己的文件夹中,并用 Xcode 分组,便于你在需要时寻找和改动。当你想找的一个类刚好就在你所期望的地方出现时,这种 Feel 倍儿爽!

用 VIPER 构建模块的另一个好处是,更容易将其扩展到多个平台。具有独立于交互器层的所有用例的应用程序逻辑,通过复用应用程序层,可以让你专注于在平板电脑端、手机端或 Mac 端构建新的用户界面。

进一步说,适用于 iPad 应用的用户界面能够重用一些 iPhone 应用的视图、视图控制器和控制器。这样的话,iPad 界面将由「超级」展示器和线框图来展现,也就是改写现成的 iPhone 端的展示器和线框构成。构建并维护跨平台的应用程序相当具有挑战性,但良好的架构可以促进模型和应用层的重用,从而让跨平台实现容易得多。

用VIPER测试

VIPER 的出现激发了关注点的分离,这使得采用 TDD 变得更加简便。交互器含有独立于任何用户界面的纯逻辑,测试起来更加容易。展示器包含用于显示准备数据的逻辑,并且独立于任何 UIKit 部件。开发这种逻辑也便于测试。

我们的首选方法是从交互器开始。UI 中的一切是服务于用例的需求。通过使用 TDD 来测试交互器的 API,你可以更好地了解用户界面和用例之间的关系。

例如,我们着眼于交互器负责的待办事项列表。寻找新的列表的策略是,要找到所有截止于下周末的待办事项,并将每个待办事项归类为到期日是今天、明天、本周晚些时候或下周。

为确保交互器找到截止于下周末的所有待办事项,我们编写第一个测试:

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

一旦知道交互器在请求相应的待办事项,我们将编写更多的测试,来确认它将任务项分配为正确的日期组(例如:今天,明天等):

- (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];
}

现在,我们已经了解交互器 API 的样子,就可以开发展示器。当展示器收到来自交互器的待办事项,我们将测试是否恰当地格式化数据,并在用户界面中显示:

- (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 自然配合。如果你先开发交互器再开发展示器,你得先打造出一套关于这些层的测试机制,并为实现用例奠定基础。你可以快速迭代这些类,因为你还不会为了测试与 UI 进行交互。之后,当你去构造视图,你就有了一个已测试的正在工作的逻辑层,并有展示层连接到该逻辑层。当你完成开发视图,成功通过所有测试后,可以首次运行该程序,希望所有部件都能运行良好。

结论

希望你这篇关于 VIPER 介绍,你也许想知道下一步该怎么办。如果你想用 VIPER 架构你的下一个应用程序,会从哪里开始呢?

这篇用 VIPER 成功实现应用的文章和示例尽量具体而明确。我们的待办事项应用程序相当简单,但也准确解释了如何使用 VIPER 来构建一个应用程序。在实际项目中,你可以根据自己的真实情况来决定要如何实践。根据我们的经验,每个项目在使用 VIPER 时,可以或多或少做出一些改变,而且所有的人都从中受益匪浅。

很多情况下,可能由于某些原因,你会想要偏离 VIPER 所指定的道路。也许你遇到了很多「bunny」对象,或者你的应用程序将受益于在故事板中使用 segues。没关系,在这种情况下,在做出决定时想一想 VIPER 所代表的精神。它的核心始终是:基于单一责任原则的架构。如果遇到问题,在决定如何向前推进时想想这个原则。

你可能想知道在现有应用中是否能使用 VIPER。遇到这种情况,你可以考虑用 VIPER 建一个新功能,许多项目都采取了这种方法。这能让你用 VIPER 构建模块,帮助你发现许多建立在单一责任原则基础上造成难以运用架构的问题。

开发软件的最大挑战在于,每个应用都迥然不同,应用程序的架构方式也不一样。对我们来说,这意味着每个应用程序都是学习和尝试新事物的机遇。如果你决定尝试 VIPER,你也会受益匪浅。

Swift 补充

不久前,在 WWDC 上苹果推出了 Swift 编程语言,这将成为 Cocoa 和 Cocoa Touch 开发的未来。现在评判 Swift 语言还太早,但我们知道,语言与我们如何设计、构建软件息息相关。我们决定用 Swift 改写 VIPER TODO 示例应用,帮助我们了解 Swift 对 VIPER 的意义。迄今为止,我们确实有所收获。以下是 Swift 的几个特点,可能会改善用 VIPER 开发应用程序的体验。

结构体

在 VIPER 中,我们采用小型的、轻量化、模型类来传递层之间的数据,比如展示器到视图。这些 PONSOs 通常只是简单地采取少量数据,并且这些类通常不会被继承。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
}

类型安全

或许,Objective-C 和 Swift 之间最大的区别在于类型处理上的不同。 Objective-C 是动态类型,而 Swift 在编译中对实现类型检查时非常严格。对于像 VIPER 的架构,当一个应用程序由多个不同层构成,类型安全对开发者效率和构架结构来说都是巨大的优势。编译器帮助你确保在层边界传递时,容器和对象始终是正确的类型。由上文可知,这便是使用结构体的最佳位置。如果一个结构体能在两层之间的边界保驾护航,由于类型安全的限制,你就能保证它永远无法逃离边界。

(完结)



你可能感兴趣的:(用 VIPER 构建 iOS 应用架构)