【编者按】本篇文章由 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 <NSObject>

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

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

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

@protocol VTDAddModuleInterface <NSObject>

- (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<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 和 Cocaa Touch 的作用是打造应用的「门面」。架构需要与应用的所有组件和平共处,但它也需要为部分框架的使用,以及处于什么位置提供建议。

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

敬请持续关注:《用 VIPER 构建 iOS 应用架构》系列(2).

原文地址:Architecting iOS Apps with VIPER

本文由OneAPM工程师编译 ,想阅读更多技术文章,请访问OneAPM官方技术博客

用 VIPER 构建 iOS 应用架构(1)的更多相关文章

  1. 用 VIPER 构建 iOS 应用架构(2)

    [编者按]本篇文章由 Jeff Gilbert 和 Conrad Stoll 共同编写,通过构建一个基础示例应用,深入了解 VIPER,并从视图.交互器等多个部件理清 VIPER 的整体布局及思路.通 ...

  2. iOS应用架构现状分析

    iOS从2007年诞生至今已有近10年的历史,10年的时间对iOS技术圈来说足够产生相当可观的沉淀,尤其这几年的技术分享氛围无论国内国外都显得异常活跃.本文就iOS架构这一主题,结合开发圈里讨论较多的 ...

  3. 一步步构建iOS路由

    什么是移动端路由层: 路由层的概念在服务端是指url请求的分层解析,将一个请求分发到对应的应用处理程序.移动端的路由层指的是将诸如App内页面访问.H5与App访问的访问请求和App间的访问请求,进行 ...

  4. 用Model-View-ViewModel构建iOS App(转)

    转载自 Model-View-ViewModel for iOS [译] 如果你已经开发一段时间的iOS应用,你一定听说过Model-View-Controller, 即MVC.MVC是构建iOS a ...

  5. 用Model-View-ViewModel构建iOS App

    如果你已经开发一段时间的iOS应用,你一定听说过Model-View-Controller,即MVC.MVC是构建iOS App的标准模式.然而,最近我已经越来越厌倦MVC的一些缺点.在本文,我将重温 ...

  6. iOS应用架构谈(三):View层的组织和调用方案(下)

    iOS客户端应用架构看似简单,但实际上要考虑的事情不少.本文作者将以系列文章的形式来回答iOS应用架构中的种种问题,本文是其中的第二篇,主要讲View层的组织和调用方案.下篇主要讨论做View层架构的 ...

  7. iOS应用架构谈(二):View层的组织和调用方案(上)

    OS客户端应用架构看似简单,但实际上要考虑的事情不少.本文作者将以系列文章的形式来回答iOS应用架构中的种种问题,本文是其中的第二篇,主要讲View层的组织和调用方案.上篇主要讲View层的代码结构. ...

  8. iOS应用架构谈(二):View层的组织和调用方案(中)

    iOS客户端应用架构看似简单,但实际上要考虑的事情不少.本文作者将以系列文章的形式来回答iOS应用架构中的种种问题,本文是其中的第二篇,主要讲View层的组织和调用方案.中篇主要讨论MVC.MVCS. ...

  9. iOS应用架构谈 view层的组织和调用方案

    当我们开始设计View层的架构时,往往是这个App还没有开始开发,或者这个App已经发过几个版本了,然后此时需要做非常彻底的重构. 一般也就是这两种时机会去做View层架构,基于这个时机的特殊性,我们 ...

随机推荐

  1. Python脚本控制的WebDriver 常用操作 <二> 关闭浏览器

    下面将模拟一个WebDriver关闭浏览器的操作 测试用例场景 在一个自动化测试脚本运行完毕后,我们很可能会采取关闭浏览器的操作,而关闭浏览器的常用操作有如下两种: close quit close  ...

  2. spring error

    <aop:config> <aop:pointcut id="allMethod" expression="execution(* a.j.shop.s ...

  3. ubuntun pptpd

    apt-get install pptpd 3.编辑pptpd.conf文件 vi /etc/pptpd.conf 取消注释下面内容 option /etc/ppp/pptpd-options loc ...

  4. XAML(3) - 附带属性

    WPF元素也可以从父元素中获得特性.例如,如果Button元素为了Canvas元素中,按钮的Top和Lef属性把父元素的名称作为前缀.这种属性成为附带属性: <Canvas> <Bu ...

  5. JavaScript 组件化开发之路(一)

    *:first-child{margin-top: 0 !important}.markdown-body>*:last-child{margin-bottom: 0 !important}.m ...

  6. GROUPING SETS、CUBE、ROLLUP

    其实还是写一个Demo 比较好 USE tempdb IF OBJECT_ID( 'dbo.T1' , 'U' )IS NOT NULL BEGIN DROP TABLE dbo.T1; END; G ...

  7. postgresql 函数 参数为复合类型

    postgresql没有存储过程,但是函数功能很强大. 在近期开发的电商管理平台中,对于产品的类目管理,设计时有个属性字段,设为字符数组,但是EF不支持数组的操作,所以在添加和修改类目时,需要对属性的 ...

  8. mousewheel滚轮事件

    原生的滚轮事件:火狐与其他浏览器使用了不同的事件 /* * 滚轮事件只有firefox比较特殊,使用DOMMouseScroll; 其他浏览器使用mousewheel; * */ // firefox ...

  9. simplexml_load_string获取xml节点里的属性值

    http://stackoverflow.com/questions/14359658/get-xml-attribute-using-simplexml-load-string 问: I am us ...

  10. MS Chart-按照数据库的最大最小时间设置X轴label.

    核心代码: Chart1.ChartAreas[0].AxisX.Interval = (Front_Max - Front_Min).Days / 2; Chart1.ChartAreas[0].A ...