【编者按】本篇文章由 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. wpf 获取DataGrid某一个单元格,设置此单元格ToolTip内容和背景颜色

    public void GetCell()        {            for (int i = 0; i < this.datagrid1.Items.Count; i++)    ...

  2. 浅谈objective—C管理内存

    这段时间被导师催着论文,调试各种BUg,也是醉了,发现很大程度上,内存出错,栈溢出,各种悲剧.那么今天就和大家一起对OC的内存管理来个探微吧.Objective-C使用一个保留计数记录了我们所创建的所 ...

  3. 算法系列5《SSF33》

    SSF33算法是以128位分组为单位进行运算,密钥长度为16字节,该算法也可以被用于安全报文传送和MAC机制密文运算. 使用SSF33算法和基于3-DES的对称加密机制使用相同长度的密钥,能够同原有的 ...

  4. eclipse java.lang.OutOfMemoryError: Java heap space

    1.手动编译运行需要添加 java -Xms256m -Xmx1024m classname 2.在eclipse中,在run as -> run configurations -> ar ...

  5. 2天驾驭DIV+CSS (基础篇)(转)

    这是去年看到的一片文章,感觉在我的学习中,有不少的影响.于是把它分享给想很快了解css的兄弟们. 基础篇[知识一] “DIV+CSS” 的叫法是不准确的[知识二] “DIV+CSS” 将你引入两大误区 ...

  6. 15个最好的Bootstrap设计工具推荐

    摘要:Bootstrap不单单是一个框架,更确切的说,它改变了整个游戏规则.该框架使得许多应用和网站的设计开发变得简便许多,而且它将大量的HTML框架普及成了产品. Bootstrap是由前Twitt ...

  7. 【译】理解与分析ios应用的崩溃报告

    源网址: http://developer.apple.com/library/ios/#technotes/tn2151/_index.html 当一个应用程序崩溃时,创建一份“崩溃报告”对于理解崩 ...

  8. Linux后门入侵检测

    蛋疼啊,服务器被入侵成肉鸡了,发出大量SYN请求到某个网站!(瞬间有种被OOXX(强)(奸)的赶脚) 泪奔ING... 源起: Linux服务器日常检查,#ps aux 发现大量httpd进程,和往常 ...

  9. bootstrap-fileinput 图片上传

    bootstrap-fileinput 源文件 在网上下载 CSS: <link href="../../static/Bootstrap/css/plugins/bootstrap- ...

  10. ThinkPHP技巧

    在php文件可以用    echo D_S()->getLastSql();来打印出  当前的sql语句