【编者按】本篇文章由 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. 理解ruby on rails中的ActiveRecord::Relation

    ActiveRecord::Relation是rails3中添加的.rails2中的finders, named_scope, with_scope 等用法,在rails3统一为一种Relation用 ...

  2. linux中fork()函数详解

    一.fork入门知识 一个进程,包括代码.数据和分配给进程的资源.fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同, ...

  3. android开发中系统自带语音模块的使用

    android开发中系统自带语音模块的使用需求:项目中需要添加语音搜索模块,增加用户体验解决过程:在网上搜到语音搜索例子,参考网上代码,加入到了自己的项目,完成产品要求.这个问题很好解决,网上能找到很 ...

  4. go again

    Introducation (1)How to organize go code (2)How to develope go package (3)How to use go tool How to ...

  5. SharePoint 2010 RBS 安装和配置的一些记录

    1.SharePoint 2010 RBS FILESTREAM Provider 的“垃圾收集”: 在SharePoint 中删除上传的文档RBS并不会在文件系统删除文档,理解只是在内容数据库删除了 ...

  6. UISlider swift

    // // ViewController.swift // UILabelTest // // Created by mac on 15/6/23. // Copyright (c) 2015年 fa ...

  7. MVC Controller 链接到 API Controller 以及反向链接

    MVC Controller 链接到 API Controller 以及反向链接 问题 想创建一个从 ASP.NET MVC controller 到 ASP.NET Web API controll ...

  8. linux设置环境变量的方法

    0.查看环境变量 export 1.直接执行命令,不过只有此次会话有效 export PATH=$PATH:/dir/I/want 2.修改profile文件 在里面加入: export PATH=& ...

  9. Scrum仪式之Sprint计划会议

    会议时间:4.15.晚八点 会议地点:基础教学楼二楼 会议进程 • 首先我们讨论了实验第一个Sprint1要实现的功能,我们的初期目标.•  然后我们进一步梳理了第一阶段的任务和需求.•  之后对任务 ...

  10. 软件工程随堂作业--随机产生30到四则运算(c语言)

    #include "stdio.h" #include "math.h" #include "stdlib.h" #include" ...