V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
OneAPM
V2EX  ›  程序员

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

  •  6
     
  •   OneAPM ·
    oneapm · 2015-08-04 17:56:42 +08:00 · 2228 次点击
    这是一个创建于 3401 天前的主题,其中的信息可能已经有所发展或是发生改变。

    ** [编者按] 本篇文章由 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。整洁的体系结构将应用程序的逻辑分配到不同的责任区。这使得依赖关系(例如数据库)更容易独立,更便于测试层与层之间的相互作用。

    用VIPER构建iOS应用架构

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

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

    基于用例的应用设计

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

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

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

    VIPER 的主要组件

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

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

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

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

    用VIPER构建iOS应用架构

    虽然 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 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问 OneAPM 官方博客

    2 条回复    2015-08-05 09:18:47 +08:00
    xi_lin
        1
    xi_lin  
       2015-08-04 21:08:28 +08:00 via iPhone
    机翻感有点重
    lynulzy
        2
    lynulzy  
       2015-08-05 09:18:47 +08:00
    没看懂
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1072 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 19:47 · PVG 03:47 · LAX 11:47 · JFK 14:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.