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

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

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

UIViewController 的确至关有用。git

在 VIPER 下,视图控制器会恰当地作好它份内的事——控制视图。咱们的应用程序有两个视图控制器,一个用于列表界面,另外一个用于增长界面。添加视图控制器的实现是很是基础的,由于它的功能是控制视图,代码以下:github

@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 客户。交互器能够汇集来自多个源的数据,提供实现用例的所需信息。而后就看显示器采集交互器反馈的数据,并格式化用于显示。数据库

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

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

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

@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 <NSObject>

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

@end


@protocol VTDAddModuleDelegate <NSObject>

- (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 应用架构(1)

原文地址:Architecting iOS Apps with VIPER

本文系 OneAPM 工程师编译整理。OneAPM 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问 OneAPM 官方博客

相关文章
相关标签/搜索