在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ iOS/ 使用 VIPER 構(gòu)建 iOS 應(yīng)用
與四軸無人機的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機工作原理
Build 過程

使用 VIPER 構(gòu)建 iOS 應(yīng)用

建筑領(lǐng)域流行這樣一句話,“我們雖然在營造建筑,但建筑也會重新塑造我們”。正如所有開發(fā)者最終領(lǐng)悟到的,這句話同樣適用于構(gòu)建軟件。

編寫代碼中至關(guān)重要的是,需要使每一部分容易被識別,賦有一個特定而明顯的目的,并與其他部分在邏輯關(guān)系中完美契合。這就是我們所說的軟件架構(gòu)。好的架構(gòu)不僅讓一個產(chǎn)品成功投入使用,還可以讓產(chǎn)品具有可維護性,并讓人不斷頭腦清醒的對它進行維護!

在這篇文章中,我們介紹了一種稱之為 VIPER 的 iOS 應(yīng)用架構(gòu)的方式。VIPER 已經(jīng)在很多大型的項目上成功實踐,但是出于本文的目的我們將通過一個待辦事項清單 (to-do app) 來介紹 VIPER 。你可以在 GitHub 上關(guān)注這個項目。

什么是 VIPER?

測試永遠不是構(gòu)建 iOS 應(yīng)用的主要部分。當(dāng)我們 (Mutual Mobile) 著手改善我們的測試實踐時,我們發(fā)現(xiàn)給 iOS 應(yīng)用寫測試代碼非常困難。因此如果想要設(shè)法改變測試的現(xiàn)狀,我們首先需要一個更好的方式來架構(gòu)應(yīng)用,我們稱之為 VIPER。

VIPER 是一個創(chuàng)建 iOS 應(yīng)用簡明構(gòu)架的程序。VIPER 可以是視圖 (View),交互器 (Interactor),展示器 (Presenter),實體 (Entity) 以及路由 (Routing) 的首字母縮寫。簡明架構(gòu)將一個應(yīng)用程序的邏輯結(jié)構(gòu)劃分為不同的責(zé)任層。這使得它更容易隔離依賴項 (如數(shù)據(jù)庫),也更容易測試各層間的邊界處的交互:

http://wiki.jikexueyuan.com/project/objc/images/13-15.jpg" alt="" />

大部分 iOS 應(yīng)用利用 MVC 構(gòu)建,使用 MVC 應(yīng)用程序架構(gòu)可以引導(dǎo)你將每一個類看做模型,視圖或控制器中的一個。但由于大部分應(yīng)用程序的邏輯不會存在于模型或視圖中,所以通常最終總是在控制器里實現(xiàn)。這就導(dǎo)致一個稱為重量級視圖控制器的問題,在這里,視圖控制器做了太多工作。為這些重量級視圖控制器瘦身并不是 iOS 開發(fā)者尋求提高代碼的質(zhì)量所要面臨的唯一挑戰(zhàn),但至少這是一個很好的開端。

VIPER 的不同層提供了明確的程序邏輯以及導(dǎo)航控制代碼來應(yīng)對這個挑戰(zhàn),利用 VIPER ,你會注意到在我們的待辦事項示例清單中的視圖控制器可以簡潔高效,意義明確地控制視圖。你也會發(fā)現(xiàn)視圖控制器中代碼和所有的其他類很容易理解,容易測試,理所當(dāng)然也更易維護。

基于用例的應(yīng)用設(shè)計

應(yīng)用通常是一些用戶用例的集合。用例也被稱為驗收標(biāo)準(zhǔn),或行為集,它們用來描述應(yīng)用的用途。清單可以根據(jù)時間,類型以及名字排序,這就是一個用例。用例是應(yīng)用程序中用來負責(zé)業(yè)務(wù)邏輯的一層,應(yīng)獨立于用戶界面的實現(xiàn),同時要足夠小,并且有良好的定義。決定如何將一個復(fù)雜的應(yīng)用分解成較小的用例非常具有挑戰(zhàn)性,并且需要長期實踐,但這對于縮小你解決的問題時所要面臨的范圍及完成的每個類的所要涉及的內(nèi)容來說,是很有幫助的。

利用 VIPER 建立一個應(yīng)用需要實施一組套件來滿足所有的用例,應(yīng)用邏輯是實現(xiàn)用例的主要組成部分,但卻不是唯一。用例也會影響用戶界面。另一個重要的方面,是要考慮用例如何與其他應(yīng)用程序的核心組件相互配合,例如網(wǎng)絡(luò)和數(shù)據(jù)持久化。組件就好比用例的插件,VIPER 則用來描述這些組件的作用是什么,如何進行交互。

我們其中一個用例,或者說待辦事項清單中其中的一個需求是可以基于用戶的選擇來將待辦事項分組。通過分離的邏輯將數(shù)據(jù)組織成一個用例,我們能夠在測試時使用戶界面代碼保持干凈,用例更易組裝,從而確保它如我們預(yù)期的方式工作。

VIPER 的主要部分

VIPER 的主要部分是:

  • 視圖:根據(jù)展示器的要求顯示界面,并將用戶輸入反饋給展示器。
  • 交互器:包含由用例指定的業(yè)務(wù)邏輯。
  • 展示器:包含為顯示(從交互器接受的內(nèi)容)做的準(zhǔn)備工作的相關(guān)視圖邏輯,并對用戶輸入進行反饋(從交互器獲取新數(shù)據(jù))。
  • 實體:包含交互器要使用的基本模型對象。
  • 路由:包含用來描述屏幕顯示和顯示順序的導(dǎo)航邏輯。

這種分隔形式同樣遵循單一責(zé)任原則。交互器負責(zé)業(yè)務(wù)分析的部分,展示器代表交互設(shè)計師,而視圖相當(dāng)于視覺設(shè)計師。

以下則是不同組件的相關(guān)圖解,并展示了他們之間是如何關(guān)聯(lián)的:

http://wiki.jikexueyuan.com/project/objc/images/13-16.png" alt="" />

雖然在應(yīng)用中 VIPER 的組件可以以任意順序?qū)崿F(xiàn),我們在這里選擇按照我們推薦的順序來進行介紹。你會注意到這個順序與構(gòu)建整個應(yīng)用的進程大致符合 -- 首先要討論的是產(chǎn)品需要做什么,以及用戶會如何與之交互。

交互器

交互器在應(yīng)用中代表著一個獨立的用例。它具有業(yè)務(wù)邏輯以操縱模型對象(實體)執(zhí)行特定的任務(wù)。交互器中的工作應(yīng)當(dāng)獨立與任何用戶界面,同樣的交互器可以同時運用于 iOS 應(yīng)用或者 OS X 應(yīng)用中。

由于交互器是一個 PONSO (Plain Old NSObject,普通的 NSObject),它主要包含了邏輯,因此很容易使用 TDD 進行開發(fā)。

示例應(yīng)用的主要用例是向用戶展示所有的待辦事項(比如任何截止于下周末的任務(wù))。此類用例的業(yè)務(wù)邏輯主要是找出今天至下周末之間將要到期的待辦事項,然后為它們分配一個相對的截止日期,比如今天,明天,本周以內(nèi),或者下周。

以下是來自 VTDListInteractor 的對應(yīng)方法:

- (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]];
    }];
}

實體

實體是被交互器操作的模型對象,并且它們只被交互器所操作。交互器永遠不會傳輸實體至表現(xiàn)層 (比如說展示器)。

實體也應(yīng)該是 PONSOs。如果你使用 Core Data,最好是將托管對象保持在你的數(shù)據(jù)層之后,交互器不應(yīng)與 NSManageObjects 協(xié)同工作。

這里是我們的待辦事項服務(wù)的實體:

@interface VTDTodoItem : NSObject

@property (nonatomic, strong)   NSDate*     dueDate;
@property (nonatomic, copy)     NSString*   name;

+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;

@end

不要詫異于你的實體僅僅是數(shù)據(jù)結(jié)構(gòu),任何依賴于應(yīng)用的邏輯都應(yīng)該放到交互器中。

展示器

展示器是一個主要包含了驅(qū)動用戶界面的邏輯的 PONSO,它總是知道何時呈現(xiàn)用戶界面。基于其收集來自用戶交互的輸入功能,它可以在合適的時候更新用戶界面并向交互器發(fā)送請求。

當(dāng)用戶點擊 “+” 鍵新建待辦事項時,addNewEntry 被調(diào)用。對于此項操作,展示器會要求 wireframe 顯示用戶界面以增加新項目:

- (void)addNewEntry
{
    [self.listWireframe presentAddInterface];
}

展示器還會從交互器接收結(jié)果并將結(jié)果轉(zhuǎn)換成能夠在視圖中有效顯示的形式。

下面是如何從交互器接受待辦事項的過程,其中包含了處理數(shù)據(jù)的過程并決定展現(xiàn)給用戶哪些內(nèi)容:

- (void)foundUpcomingItems:(NSArray*)upcomingItems
{
    if ([upcomingItems count] == 0)
    {
        [self.userInterface showNoContentMessage];
    }
    else
    {
        [self updateUserInterfaceWithUpcomingItems:upcomingItems];
    }
}

實體永遠不會由交互器傳輸給展示器,取而代之,那些無行為的簡單數(shù)據(jù)結(jié)構(gòu)會從交互器傳輸?shù)秸故酒髂抢?。這就防止了那些“真正的工作”在展示器那里進行,展示器只能負責(zé)準(zhǔn)備那些在視圖里顯示的數(shù)據(jù)。

視圖

視圖一般是被動的,它通常等待展示器下發(fā)需要顯示的內(nèi)容,而不會向其索取數(shù)據(jù)。視圖(例如登錄界面的登錄視圖控件)所定義的方法應(yīng)該允許展示器在高度抽象的層次與之交流。展示器通過內(nèi)容進行表達,而不關(guān)心那些內(nèi)容所顯示的樣子。展示器不知道 UILabelUIButton 等的存在,它只知道其中包含的內(nèi)容以及何時需要顯示。內(nèi)容如何被顯示是由視圖來進行控制的。

視圖是一個抽象的接口 (Interface),在 Objective-C 中使用協(xié)議被定義。一個 UIViewController 或者它的一個子類會實現(xiàn)視圖協(xié)議。比如我們的示例中 “添加” 界面會有以下接口:

@protocol VTDAddViewInterface <NSObject>

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

@end

視圖和視圖控制器同樣會操縱用戶界面和相關(guān)輸入。因為通常來說視圖控制器是最容易處理這些輸入和執(zhí)行某些操作的地方,所以也就不難理解為什么視圖控制器總是這么大了。為了使視圖控制器保持苗條,我們需要使它們在用戶進行相關(guān)操作的時候可以有途徑來通知相關(guān)部分。視圖控制器不應(yīng)當(dāng)根據(jù)這些行為進行相關(guān)決定,但是它應(yīng)當(dāng)將發(fā)生的事件傳遞到能夠做決定的部分。

在我們的例子中,Add View Controller 有一個事件處理的屬性,它實現(xiàn)了如下接口:

@protocol VTDAddModuleInterface <NSObject>

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

@end

當(dāng)用戶點擊取消鍵的時候,視圖控制器告知這個事件處理程序用戶需要其取消這次添加的動作。這樣一來,事件處理程序便可以處理關(guān)閉 add view controller 并告知列表視圖進行更新。

視圖和展示器之間邊界處是一個使用 ReactiveCocoa 的好地方。在這個示例中,視圖控制器可以返回一個代表按鈕操作的信號。這將允許展示器在不打破職責(zé)分離的前提下輕松地對那些信號進行響應(yīng)。

路由

屏幕間的路徑會在交互設(shè)計師創(chuàng)建的線框 (wireframes) 里進行定義。在 VIPER 中,路由是由兩個部分來負責(zé)的:展示器和線框。一個線框?qū)ο蟀?UIWindow,UINavigationController,UIViewController 等部分,它負責(zé)創(chuàng)建視圖/視圖控制器并將其裝配到窗口中。

由于展示器包含了響應(yīng)用戶輸入的邏輯,因此它就擁有知曉何時導(dǎo)航至另一個屏幕以及具體是哪一個屏幕的能力。而同時,線框知道如何進行導(dǎo)航。在兩者結(jié)合起來的情況下,展示器可以使用線框來進行實現(xiàn)導(dǎo)航功能,它們兩者一起描述了從一個屏幕至另一個屏幕的路由過程。

線框同時也明顯是一個處理導(dǎo)航轉(zhuǎn)場動畫的地方。來看看這個 add wireframe 中的例子吧:

@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

應(yīng)用使用了自定義的視圖控制器轉(zhuǎn)場來呈現(xiàn) add view controller。因為線框部件負責(zé)實施這個轉(zhuǎn)場,所以它成為了 add view controller 轉(zhuǎn)場的委托,并且返回適當(dāng)?shù)霓D(zhuǎn)場動畫。

利用 VIPER 組織應(yīng)用組件

iOS 應(yīng)用的構(gòu)架需要考慮到 UIKit 和 Cocoa Touch 是建立應(yīng)用的主要工具。架構(gòu)需要和應(yīng)用的所有組件都能夠和平相處,但又需要為如何使用框架的某些部分以及它們應(yīng)該在什么位置提供一些指導(dǎo)和建議。

iOS 應(yīng)用程序的主力是 UIViewController,我們不難想象找一個競爭者來取代 MVC 就可以避免大量使用視圖控制器。但是視圖控制器現(xiàn)在是這個平臺的核心:它們處理設(shè)備方向的變化,回應(yīng)用戶的輸入,和類似導(dǎo)航控制器之類的系統(tǒng)系統(tǒng)組件集成得很好,而現(xiàn)在在 iOS 7 中又能實現(xiàn)自定義屏幕之間的轉(zhuǎn)換,功能實在是太強大了。

有了 VIPER,視圖控制器便就能真正的做它本來應(yīng)該做的事情了,那就是控制視圖。 我們的待辦事項應(yīng)擁有兩個視圖控制器,一個是列表視圖,另一個是新建待辦。因為 add view controller 要做的所有事情就是控制視圖,所以實現(xiàn)起來非常的簡單基礎(chǔ):

@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

應(yīng)用在接入網(wǎng)絡(luò)以后會變得更有用處,但是究竟該在什么時候聯(lián)網(wǎng)呢?又由誰來負責(zé)啟動網(wǎng)絡(luò)連接呢?典型的情況下,由交互器來啟動網(wǎng)絡(luò)連接操作的項目,但是它不會直接處理網(wǎng)絡(luò)代碼。它會尋找一個像是 network manager 或者 API client 這樣的依賴項。交互器可能聚合來自多個源的數(shù)據(jù)來提供所需的信息,從而完成一個用例。最終,就由展示器來采集交互器反饋的數(shù)據(jù),然后組織并進行展示。

數(shù)據(jù)存儲模塊負責(zé)提供實體給交互器。因為交互器要完成業(yè)務(wù)邏輯,因此它需要從數(shù)據(jù)存儲中獲取實體并操縱它們,然后將更新后的實體再放回數(shù)據(jù)存儲中。數(shù)據(jù)存儲管理實體的持久化,而實體應(yīng)該對數(shù)據(jù)庫全然不知,正因如此,實體并不知道如何對自己進行持久化。

交互器同樣不需要知道如何將實體持久化,有時交互器更希望使用一個 data manager 來使其與數(shù)據(jù)存儲的交互變得容易。Data manager 可以處理更多的針對存儲的操作,比如創(chuàng)建獲取請求,構(gòu)建查詢等等。這就使交互器能夠?qū)⒏嗟淖⒁饬Ψ旁趹?yīng)用邏輯上,而不必再了解實體是如何被聚集或持久化的。下面我們舉一個例子來說明使用 data manager 有意義的,這個例子假設(shè)你在使用 Core Data。這是示例應(yīng)用程序的 data manager 的接口:

@interface VTDListDataManager : NSObject

@property (nonatomic, strong) VTDCoreDataStore *dataStore;

- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;

@end

當(dāng)使用 TDD 來開發(fā)一個交互器時,是可以用一個測試用的模擬存儲來代替生產(chǎn)環(huán)境的數(shù)據(jù)存儲的。避免與遠程服務(wù)器通訊(網(wǎng)絡(luò)服務(wù))以及避免讀取磁盤(數(shù)據(jù)庫)可以加快你測試的速度并加強其可重復(fù)性。

將數(shù)據(jù)存儲保持為一個界限清晰的特定層的原因之一是,這可以讓你延遲選擇一個特定的持久化技術(shù)。如果你的數(shù)據(jù)存儲是一個獨立的類,那你就可以使用一個基礎(chǔ)的持久化策略來開始你的應(yīng)用,然后等到有意義的時候升級至 SQLite 或者 Core Data。而因為數(shù)據(jù)存儲層的存在,你的應(yīng)用代碼庫中就不需要改變?nèi)魏螙|西。

在 iOS 的項目中使用 Core Data 經(jīng)常比構(gòu)架本身還容易引起更多爭議。然而,利用 VIPER 來使用 Core Data 將給你帶來使用 Core Data 的前所未有的良好體驗。在持久化數(shù)據(jù)的工具層面上,Core Data 可以保持快速存取和低內(nèi)存占用方面,簡直是個神器。但是有個很惱人的地方,它會像觸須一樣把 NSManagedObjectContext 延伸至你所有的應(yīng)用實現(xiàn)文件中,特別是那些它們不該待的地方。VIPER 可以使 Core Data 待在正確的地方:數(shù)據(jù)存儲層。

在待辦事項示例中,應(yīng)用僅有的兩部分知道使用了 Core Data,其一是數(shù)據(jù)存儲本身,它負責(zé)建立 Core Data 堆棧;另一個是 data manager。Data manager 執(zhí)行了獲取請求,將數(shù)據(jù)存儲返回的 NSManagedObject 對象轉(zhuǎn)換為標(biāo)準(zhǔn)的 PONSO 模型對象,并傳輸回業(yè)務(wù)邏輯層。這樣一來,應(yīng)用程序核心將不再依賴于 Core Data,附加得到的好處是,你也再也不用擔(dān)心過期數(shù)據(jù) (stale) 和沒有良好組織的多線程 NSManagedObjects 來糟蹋你的工作成果了。

在通過請求訪問 Core Data 存儲時,data manager 中看起來是這樣的:

@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

與 Core Data 一樣極富爭議的恐怕就是 UI 故事板了。故事板具有很多有用的功能,如果完全忽視它將會是一個錯誤。然而,調(diào)用故事版所能提供的所有功能來完成 VIPER 的所有目標(biāo)仍然是很困難的。

我們所能做出的妥協(xié)就是選擇不使用 segues 。有時候使用 segues 是有效的,但是使用 segues 的危險性在于它們很難原封不動地保持屏幕之間的分離,以及 UI 和應(yīng)用邏輯之間的分離。一般來說,如果實現(xiàn) prepareForSegue 方法是必須的話,我們就盡量不去使用 segues。

除此之外,故事板是一個實現(xiàn)用戶界面布局有效方法,特別是在使用自動布局的時候。我們選擇在實現(xiàn)待辦事項兩個界面的實例中使用故事板,并且使用這樣的代碼來執(zhí)行自己的導(dǎo)航操作。

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 構(gòu)建模塊

一般在使用 VIPER 的時候,你會發(fā)現(xiàn)一個屏幕或一組屏幕傾向于聚在一起作為一個模塊。模塊可以以多種形式體現(xiàn),但一般最好把它想成是一種特性。在播客應(yīng)用中,一個模塊可能是音頻播放器或訂閱瀏覽器。然而在我們的待辦事項應(yīng)用中,列表和添加事項的屏幕都將作為單獨的模塊被建立。

將你的應(yīng)用作為一組模塊來設(shè)計有很多好處,其中之一就是模塊可以有非常明確和定義良好的接口,并且獨立于其他的模塊。這就使增加或者移除特性變得更加簡單,也使在界面中向用戶展示各種可變模塊變得更加簡單。

我們希望能將待辦事項中各模塊之間分隔更加明確,我們?yōu)樘砑幽K定義了兩個協(xié)議。一個是模塊接口,它定義了模塊可以做什么;另一個則是模塊的代理,用來描述該模塊做了什么。例如:

@protocol VTDAddModuleInterface <NSObject>

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

@end

@protocol VTDAddModuleDelegate <NSObject>

- (void)addModuleDidCancelAddAction;
- (void)addModuleDidSaveAddAction;

@end

因為模塊必須要被展示,才能對用戶產(chǎn)生價值,所以模塊的展示器通常需要實現(xiàn)模型的接口。當(dāng)另一個模型想要展現(xiàn)當(dāng)前模塊時,它的展示器就需要實現(xiàn)模型的委托協(xié)議,這樣它就能在展示時知道當(dāng)前模塊做了些什么。

一個模塊可能包括實體,交互器和管理器的通用應(yīng)用邏輯層,這些通常可用于多個屏幕。當(dāng)然,這取決于這些屏幕之間的交互及它們的相似度。一個模塊可以像在待辦事項列表里面一樣,簡單的只代表一個屏幕。這樣一來,應(yīng)用邏輯層對于它的特定模塊的行為來說就非常特有了。

模塊同樣是組織代碼的簡便途徑。將模塊所有的編碼都放在它自己的文件夾中并在 Xcode 中建一個 group,這會在你需要尋找和改變更加容易。當(dāng)你在要尋找一個類時,它恰到好處地就在你所期待的地方,這種感覺真是無法形容的棒。

利用 VIPER 建立模塊的另一個好處是它使得擴展到多平臺時變得更加簡單。獨立在交互器層中的所有用例的應(yīng)用邏輯允許你可以專注于為平板,電話或者 Mac 構(gòu)建新的用戶界面,同時可以重用你的應(yīng)用層。

進一步來說,iPad 應(yīng)用的用戶界面能夠?qū)⒉糠?iPhone 應(yīng)用的視圖,視圖控制器及展示器進行再利用。在這種情況下,iPad 屏幕將由 ‘super’ 展示器和線框來代表,這樣可以利用 iPhone 使用過的展示器和線框來組成屏幕。建立進而維護一個跨多平臺的應(yīng)用是一個巨大的挑戰(zhàn),但是好的構(gòu)架可以對整個模型和應(yīng)用層的再利用有大幅度的提升,并使其實現(xiàn)起來更加容易。

利用 VIPER 進行測試

VIPER 的出現(xiàn)激發(fā)了一個關(guān)注點的分離,這使得采用 TDD 變得更加簡便。交互器包含獨立與任何 UI 的純粹邏輯,這使測試驅(qū)動開發(fā)更加簡單。同時展示器包含用來為顯示準(zhǔn)備數(shù)據(jù)的邏輯,并且它也獨立于任何一個 UIKit 部件。對于這個邏輯的開發(fā)也很容易用測試來驅(qū)動。

我們更傾向于先從交互器下手。用戶界面里所有部分都服務(wù)于用例,而通過采用 TDD 來測試驅(qū)動交互器的 API 可以讓你對用戶界面和用例之間的關(guān)系有一個更好的了解。

作為實例,我們來看一下負責(zé)待辦事項列表的交互器。尋找待辦事項的策略是要找出所有的將在下周末前截止的項目,并將這些項目分別歸類至截止于今天,明天,本周或者下周。

我們編寫的第一個測試是為了保證交互器能夠找到所有的截止于下周末的待辦事項:

- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek
{
    [[self.dataManager expect] todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY];
    [self.interactor findUpcomingItems];
}

一旦知道了交互器找到了正確的待辦事項后,我們就需要編寫幾個小測試用來確認它確實將待辦事項分配到了正確的相對日期組內(nèi)(比如說今天,明天,等等)。

- (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];
}

既然我們已經(jīng)知道了交互器的 API 長什么樣,接下來就是開發(fā)展示器。一旦展示器接收到了交互器傳來的待辦事項,我們就需要測試看看我們是否適當(dāng)?shù)膶?shù)據(jù)進行格式化并且在用戶界面中正確的顯示它。

- (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]];
}

同樣需要測試的是應(yīng)用是否在用戶想要新建待辦事項時正確啟動了相應(yīng)操作:

- (void)testAddNewToDoItemActionPresentsAddToDoUI
{
    [[self.wireframe expect] presentAddInterface];

    [self.presenter addNewEntry];
}

這時我們可以開發(fā)視圖功能了,并且在沒有待辦事項的時候我們想要展示一個特殊的信息。

- (void)testShowingNoContentMessageShowsNoContentView
{
    [self.view showNoContentMessage];

    XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");
}

有待辦事項出現(xiàn)時,我們要確保列表是顯示出來的:

- (void)testShowingUpcomingItemsShowsTableView
{
    [self.view showUpcomingDisplayData:nil];

    XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");
}

首先建立交互器是一種符合 TDD 的自然規(guī)律。如果你首先開發(fā)交互器,緊接著是展示器,你就可以首先建立一個位于這些層的套件測試,并且為實現(xiàn)這是實例奠定基礎(chǔ)。由于你不需要為了測試它們而去與用戶界面進行交互,所以這些類可以進行快速迭代。在你需要開發(fā)視圖的時候,你會有一個可以工作并測試過的邏輯和表現(xiàn)層來與其進行連接。在快要完成對視圖的開發(fā)時,你會發(fā)現(xiàn)第一次運行程序時所有部件都運行良好,因為你所有已通過的測試已經(jīng)告訴你它可以工作。

結(jié)論

我們希望你喜歡這篇對 VIPER 的介紹?;蛟S你們都很好奇接下來應(yīng)該做什么,如果你希望通過 VIPER 來對你下一個應(yīng)用進行設(shè)計,該從哪里開始呢?

我們竭盡全力使這篇文章和我們利用 VIPER 實現(xiàn)的應(yīng)用實例足夠明確并且進行了很好的定義。我們的待辦事項里列表程序相當(dāng)直接簡單,但是它準(zhǔn)確地解釋了如何利用 VIPER 來建立一個應(yīng)用。在實際的項目中,你可以根據(jù)你自己的挑戰(zhàn)和約束條件來決定要如何實踐這個例子。根據(jù)以往的經(jīng)驗,我們的每個項目在使用 VIPER 時都或多或少地改變了一些策略,但它們無一例外的都從中得益,找到了正確的方向。

很多情況下由于某些原因,你可能會想要偏離 VIPER 所指引的道路??赡苣阌龅搅撕芏? 對象,或者你的應(yīng)用使用了故事板的 segues。沒關(guān)系的,在這些情況下,你只需要在做決定時稍微考慮下 VIPER 所代表的精神就好。VIPER 的核心在于它是建立在單一責(zé)任原則上的架構(gòu)。如果你碰到了些許麻煩,想想這些原則再考慮如何前進。

你一定想知道在現(xiàn)有的應(yīng)用中能否只用 VIPER 。在這種情況下,你可以考慮使用 VIPER 構(gòu)建新的特性。我們許多現(xiàn)有項目都使用了這個方法。你可以利用 VIPER 建立一個模塊,這能幫助你發(fā)現(xiàn)許多建立在單一責(zé)任原則基礎(chǔ)上造成難以運用架構(gòu)的現(xiàn)有問題。

軟件開發(fā)最偉大的事情之一就是每個應(yīng)用程序都是不同的,而設(shè)計每個應(yīng)用的架構(gòu)的方式也是不同的。這就意味著每個應(yīng)用對于我們來說都是一個學(xué)習(xí)和嘗試的機遇,如果你決定開始使用 VIPER,你會受益匪淺。感謝你的閱讀。

Swift 補充

蘋果上周在 WWDC 介紹了一門稱之為 Swift 的編程語言來作為 Cocoa 和 Cocoa Touch 開發(fā)的未來?,F(xiàn)在發(fā)表關(guān)于 Swift 的完整意見還為時尚早,但眾所周知編程語言對我們?nèi)绾卧O(shè)計和構(gòu)建應(yīng)用有著重大影響。我們決定使用 Swift 重寫我們的待辦事項清單,幫助我們學(xué)習(xí)它對 VIPER 意味著什么。至今為止,收獲頗豐。Swift 中的一些特性對于構(gòu)建應(yīng)用的體驗有著顯著的提升。

結(jié)構(gòu)體

在 VIPER 中我們使用小型,輕量級的 model 類來在比如從展示器到視圖這樣不同的層間傳遞數(shù)據(jù)。這些 PONSOs 通常是只是簡單地帶有少量數(shù)據(jù),并且通常這些類不會被繼承。Swift 的結(jié)構(gòu)體非常適合這個情況。下面的結(jié)構(gòu)體的例子來自 VIPER Swift。這個結(jié)構(gòu)體需要被判斷是否相等,所以我們重載了 == 操作符來比較這個類型的兩個實例。

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 的最大區(qū)別是它們在對于類型處理上的不同。 Objective-C 是動態(tài)類型,而 Swift 故意在編譯時做了嚴(yán)格的類型檢查。對于一個類似 VIPER 的架構(gòu), 應(yīng)用由不同層構(gòu)成,類型安全是提升程序員效率和設(shè)計架構(gòu)有非常大的好處。編譯器幫助你確保正確類型的容器和對象在層的邊界傳遞。如上所示,這是一個使用結(jié)構(gòu)體的好地方。如果一個結(jié)構(gòu)體的被設(shè)計為存在于兩層之間,那么由于類型安全,你可以保證它將永遠無法脫離這些層之間。

擴展閱讀