iOS開發—使用VIPER構建iOS應用

建築領域流行這樣一句話,「咱們雖然在營造建築,但建築也會從新塑造咱們」。正如全部開發者最終領悟到的,這句話一樣適用於構建軟件。
 
編寫代碼中相當重要的是,須要使每一部分容易被識別,賦有一個特定而明顯的目的,並與其餘部分在邏輯關係中完美契合。這就是咱們所說的軟件架構。好的架構不只讓一個產品成功投入使用,還可讓產品具備可維護性,並讓人不斷頭腦清醒的對它進行維護!
 
在這篇文章中,咱們介紹了一種稱之爲  VIPER 的 iOS 應用架構的方式。VIPER 已經在不少大型的項目上成功實踐,可是出於本文的目的咱們將經過一個待辦事項清單 (to-do app) 來介紹 VIPER 。你能夠在  GitHub 上關注這個項目。
 
什麼是 VIPER?
測試永遠不是構建 iOS 應用的主要部分。當咱們 ( Mutual Mobile) 着手改善咱們的測試實踐時,咱們發現給 iOS 應用寫測試代碼很是困難。所以若是想要設法改變測試的現狀,咱們首先須要一個更好的方式來架構應用,咱們稱之爲 VIPER。
 
VIPER 是一個建立 iOS 應用 簡明構架的程序。VIPER 能夠是視圖 (View),交互器 (Interactor),展現器 (Presenter),實體 (Entity) 以及路由 (Routing) 的首字母縮寫。簡明架構將一個應用程序的邏輯結構劃分爲不一樣的責任層。這使得它更容易隔離依賴項 (如數據庫),也更容易測試各層間的邊界處的交互:
 
大部分 iOS 應用利用 MVC 構建,使用 MVC 應用程序架構能夠引導你將每個類看作模型,視圖或控制器中的一個。但因爲大部分應用程序的邏輯不會存在於模型或視圖中,因此一般最終老是在控制器裏實現。這就致使一個稱爲 重量級視圖控制器的問題,在這裏,視圖控制器作了太多工做。爲這些重量級視圖控制器 瘦身並非 iOS 開發者尋求提升代碼的質量所要面臨的惟一挑戰,但至少這是一個很好的開端。
 
VIPER 的不一樣層提供了明確的程序邏輯以及導航控制代碼來應對這個挑戰,利用 VIPER ,你會注意到在咱們的待辦事項示例清單中的視圖控制器能夠簡潔高效,意義明確地控制視圖。你也會發現視圖控制器中代碼和全部的其餘類很容易理解,容易測試,理所固然也更易維護。
 
基於用例的應用設計
應用一般是一些用戶用例的集合。用例也被稱爲驗收標準,或行爲集,它們用來描述應用的用途。清單能夠根據時間,類型以及名字排序,這就是一個用例。用例是應用程序中用來負責業務邏輯的一層,應獨立於用戶界面的實現,同時要足夠小,而且有良好的定義。決定如何將一個複雜的應用分解成較小的用例很是具備挑戰性,而且須要長期實踐,但這對於縮小你解決的問題時所要面臨的範圍及完成的每一個類的所要涉及的內容來講,是頗有幫助的。
 
利用 VIPER 創建一個應用須要實施一組套件來知足全部的用例,應用邏輯是實現用例的主要組成部分,但卻不是惟一。用例也會影響用戶界面。另外一個重要的方面,是要考慮用例如何與其餘應用程序的核心組件相互配合,例如網絡和數據持久化。組件就比如用例的插件,VIPER 則用來描述這些組件的做用是什麼,如何進行交互。
 
咱們其中一個用例,或者說待辦事項清單中其中的一個需求是能夠基於用戶的選擇來將待辦事項分組。經過分離的邏輯將數據組織成一個用例,咱們可以在測試時使用戶界面代碼保持乾淨,用例更易組裝,從而確保它如咱們預期的方式工做。
 
VIPER 的主要部分
VIPER 的主要部分是:
 
視圖:根據展現器的要求顯示界面,並將用戶輸入反饋給展現器。
交互器:包含由用例指定的業務邏輯。
展現器:包含爲顯示(從交互器接受的內容)作的準備工做的相關視圖邏輯,並對用戶輸入進行反饋(從交互器獲取新數據)。
實體:包含交互器要使用的基本模型對象。
路由:包含用來描述屏幕顯示和顯示順序的導航邏輯。
 
這種分隔形式一樣遵循 單一責任原則。交互器負責業務分析的部分,展現器表明交互設計師,而視圖至關於視覺設計師。
 
如下則是不一樣組件的相關圖解,並展現了他們之間是如何關聯的:
雖然在應用中 VIPER 的組件能夠以任意順序實現,咱們在這裏選擇按照咱們推薦的順序來進行介紹。你會注意到這個順序與構建整個應用的進程大體符合 -- 首先要討論的是產品須要作什麼,以及用戶會如何與之交互。
 
交互器
交互器在應用中表明着一個獨立的用例。它具備業務邏輯以操縱模型對象(實體)執行特定的任務。交互器中的工做應當獨立與任何用戶界面,一樣的交互器能夠同時運用於 iOS 應用或者 OS X 應用中。
 
因爲交互器是一個 PONSO (Plain Old NSObject,普通的 NSObject),它主要包含了邏輯,所以很容易使用 TDD 進行開發。
 
示例應用的主要用例是向用戶展現全部的待辦事項(好比任何截止於下週末的任務)。此類用例的業務邏輯主要是找出今天至下週末之間將要到期的待辦事項,而後爲它們分配一個相對的截止日期,好比今天,明天,本週之內,或者下週。
 
如下是來自 VTDListInteractor 的對應方法:
  1. - (void)findUpcomingItems 
  2.     __weak typeof(self) welf = self; 
  3.     NSDate* today = [self.clock today]; 
  4.     NSDate* endOfNextWeek = [[NSCalendar currentCalendar] dateForEndOfFollowingWeekWithDate:today]; 
  5.     [self.dataManager todoItemsBetweenStartDate:today endDate:endOfNextWeek completionBlock:^(NSArray* todoItems) { 
  6.         [welf.output foundUpcomingItems:[welf upcomingItemsFromToDoItems:todoItems]]; 
  7.     }]; 
 
實體
實體是被交互器操做的模型對象,而且它們只被交互器所操做。交互器永遠不會傳輸實體至表現層 (好比說展現器)。
 
實體也應該是 PONSOs。若是你使用 Core Data,最好是將託管對象保持在你的數據層以後,交互器不該與 NSManageObjects 協同工做。
 
這裏是咱們的待辦事項服務的實體:
  1. @interface VTDTodoItem : NSObject 
  2.  
  3. @property (nonatomic, strong)   NSDate*     dueDate; 
  4. @property (nonatomic, copy)     NSString*   name; 
  5.  
  6. + (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name; 
  7.  
  8. @end 
不要詫異於你的實體僅僅是數據結構,任何依賴於應用的邏輯都應該放到交互器中。
 
展現器
展現器是一個主要包含了驅動用戶界面的邏輯的 PONSO,它老是知道什麼時候呈現用戶界面。基於其收集來自用戶交互的輸入功能,它能夠在合適的時候更新用戶界面並向交互器發送請求。
 
當用戶點擊 「+」 鍵新建待辦事項時,addNewEntry 被調用。對於此項操做,展現器會要求 wireframe 顯示用戶界面以增長新項目:
  1. - (void)addNewEntry 
  2.     [self.listWireframe presentAddInterface]; 
展現器還會從交互器接收結果並將結果轉換成可以在視圖中有效顯示的形式。
 
下面是如何從交互器接受待辦事項的過程,其中包含了處理數據的過程並決定展示給用戶哪些內容:
  1. - (void)foundUpcomingItems:(NSArray*)upcomingItems 
  2.     if ([upcomingItems count] == 0) 
  3.     { 
  4.         [self.userInterface showNoContentMessage]; 
  5.     } 
  6.     else 
  7.     { 
  8.         [self updateUserInterfaceWithUpcomingItems:upcomingItems]; 
  9.     } 
實體永遠不會由交互器傳輸給展現器,取而代之,那些無行爲的簡單數據結構會從交互器傳輸到展現器那裏。這就防止了那些「真正的工做」在展現器那裏進行,展現器只能負責準備那些在視圖裏顯示的數據。
 
視圖
視圖通常是被動的,它一般等待展現器下發須要顯示的內容,而不會向其索取數據。視圖(例如登陸界面的登陸視圖控件)所定義的方法應該容許展現器在高度抽象的層次與之交流。展現器經過內容進行表達,而不關心那些內容所顯示的樣子。展現器不知道 UILabel,UIButton 等的存在,它只知道其中包含的內容以及什麼時候須要顯示。內容如何被顯示是由視圖來進行控制的。
 
視圖是一個抽象的接口 (Interface),在 Objective-C 中使用協議被定義。一個 UIViewController 或者它的一個子類會實現視圖協議。好比咱們的示例中 「添加」 界面會有如下接口:
  1. @protocol VTDAddViewInterface  
  2.  
  3. - (void)setEntryName:(NSString *)name; 
  4. - (void)setEntryDueDate:(NSDate *)date; 
  5.  
  6. @end 
視圖和視圖控制器一樣會操縱用戶界面和相關輸入。由於一般來講視圖控制器是最容易處理這些輸入和執行某些操做的地方,因此也就不難理解爲何視圖控制器老是這麼大了。爲了使視圖控制器保持苗條,咱們須要使它們在用戶進行相關操做的時候能夠有途徑來通知相關部分。視圖控制器不該當根據這些行爲進行相關決定,可是它應當將發生的事件傳遞到可以作決定的部分。
 
在咱們的例子中,Add View Controller 有一個事件處理的屬性,它實現了以下接口:
  1. @protocol VTDAddModuleInterface  
  2.  
  3. - (void)cancelAddAction; 
  4. - (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate 
  5.  
  6. @end 
當用戶點擊取消鍵的時候,視圖控制器告知這個事件處理程序用戶須要其取消此次添加的動做。這樣一來,事件處理程序即可以處理關閉 add view controller 並告知列表視圖進行更新。
 
視圖和展現器之間邊界處是一個使用  ReactiveCocoa 的好地方。在這個示例中,視圖控制器能夠返回一個表明按鈕操做的信號。這將容許展現器在不打破職責分離的前提下輕鬆地對那些信號進行響應。
 
路由
屏幕間的路徑會在交互設計師建立的線框 (wireframes) 裏進行定義。在 VIPER 中,路由是由兩個部分來負責的:展現器和線框。一個線框對象包括 UIWindow,UINavigationController,UIViewController 等部分,它負責建立視圖/視圖控制器並將其裝配到窗口中。
 
因爲展現器包含了響應用戶輸入的邏輯,所以它就擁有知曉什麼時候導航至另外一個屏幕以及具體是哪個屏幕的能力。而同時,線框知道如何進行導航。在二者結合起來的狀況下,展現器可使用線框來進行實現導航功能,它們二者一塊兒描述了從一個屏幕至另外一個屏幕的路由過程。
 
線框同時也明顯是一個處理導航轉場動畫的地方。來看看這個 add wireframe 中的例子吧:
  1. @implementation VTDAddWireframe 
  2.  
  3. - (void)presentAddInterfaceFromViewController:(UIViewController *)viewController  
  4.     VTDAddViewController *addViewController = [self addViewController]; 
  5.     addViewController.eventHandler = self.addPresenter; 
  6.     addViewController.modalPresentationStyle = UIModalPresentationCustom; 
  7.     addViewController.transitioningDelegate = self; 
  8.  
  9.     [viewController presentViewController:addViewController animated:YES completion:nil]; 
  10.  
  11.     self.presentedViewController = viewController; 
  12.  
  13. #pragma mark - UIViewControllerTransitioningDelegate Methods 
  14.  
  15. - (id)animationControllerForDismissedController:(UIViewController *)dismissed  
  16.     return [[VTDAddDismissalTransition alloc] init]; 
  17.  
  18. - (id)animationControllerForPresentedController:(UIViewController *)presented 
  19.                                                                   presentingController:(UIViewController *)presenting 
  20.                                                                       sourceController:(UIViewController *)source  
  21.     return [[VTDAddPresentationTransition alloc] init]; 
  22.  
  23. @end 
應用使用了自定義的視圖控制器轉場來呈現 add view controller。由於線框部件負責實施這個轉場,因此它成爲了 add view controller 轉場的委託,而且返回適當的轉場動畫。
 
利用 VIPER 組織應用組件
iOS 應用的構架須要考慮到 UIKit 和 Cocoa Touch 是創建應用的主要工具。架構須要和應用的全部組件都可以和平相處,但又須要爲如何使用框架的某些部分以及它們應該在什麼位置提供一些指導和建議。
 
iOS 應用程序的主力是 UIViewController,咱們不難想象找一個競爭者來取代 MVC 就能夠避免大量使用視圖控制器。可是視圖控制器如今是這個平臺的核心:它們處理設備方向的變化,迴應用戶的輸入,和相似導航控制器之類的系統系統組件集成得很好,而如今在 iOS 7 中又能實現自定義屏幕之間的轉換,功能實在是太強大了。
 
有了 VIPER,視圖控制器便就能真正的作它原本應該作的事情了,那就是控制視圖。 咱們的待辦事項應擁有兩個視圖控制器,一個是列表視圖,另外一個是新建待辦。由於 add view controller 要作的全部事情就是控制視圖,因此實現起來很是的簡單基礎:
  1. @implementation VTDAddViewController 
  2.  
  3. - (void)viewDidAppear:(BOOL)animated  
  4.     [super viewDidAppear:animated]; 
  5.  
  6.     UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self 
  7.                                                                                         action:@selector(dismiss)]; 
  8.     [self.transitioningBackgroundView addGestureRecognizer:gestureRecognizer]; 
  9.     self.transitioningBackgroundView.userInteractionEnabled = YES; 
  10.  
  11. - (void)dismiss  
  12.     [self.eventHandler cancelAddAction]; 
  13.  
  14. - (void)setEntryName:(NSString *)name  
  15.     self.nameTextField.text = name; 
  16.  
  17. - (void)setEntryDueDate:(NSDate *)date  
  18.     [self.datePicker setDate:date]; 
  19.  
  20. - (IBAction)save:(id)sender  
  21.     [self.eventHandler saveAddActionWithName:self.nameTextField.text 
  22.                                      dueDate:self.datePicker.date]; 
  23.  
  24. - (IBAction)cancel:(id)sender  
  25.     [self.eventHandler cancelAddAction]; 
  26.  
  27.  
  28. #pragma mark - UITextFieldDelegate Methods 
  29.  
  30. - (BOOL)textFieldShouldReturn:(UITextField *)textField  
  31.     [textField resignFirstResponder]; 
  32.  
  33.     return YES; 
  34.  
  35. @end 
應用在接入網絡之後會變得更有用處,可是究竟該在何時聯網呢?又由誰來負責啓動網絡鏈接呢?典型的狀況下,由交互器來啓動網絡鏈接操做的項目,可是它不會直接處理網絡代碼。它會尋找一個像是 network manager 或者 API client 這樣的依賴項。交互器可能聚合來自多個源的數據來提供所需的信息,從而完成一個用例。最終,就由展現器來採集交互器反饋的數據,而後組織並進行展現。
 
數據存儲模塊負責提供實體給交互器。由於交互器要完成業務邏輯,所以它須要從數據存儲中獲取實體並操縱它們,而後將更新後的實體再放回數據存儲中。數據存儲管理實體的持久化,而實體應該對數據庫全然不知,正因如此,實體並不知道如何對本身進行持久化。
 
交互器一樣不須要知道如何將實體持久化,有時交互器更但願使用一個 data manager 來使其與數據存儲的交互變得容易。Data manager 能夠處理更多的針對存儲的操做,好比建立獲取請求,構建查詢等等。這就使交互器可以將更多的注意力放在應用邏輯上,而沒必要再瞭解實體是如何被彙集或持久化的。下面咱們舉一個例子來講明使用 data manager 有意義的,這個例子假設你在使用 Core Data。這是示例應用程序的 data manager 的接口:
  1. @interface VTDListDataManager : NSObject 
  2.  
  3. @property (nonatomic, strong) VTDCoreDataStore *dataStore; 
  4.  
  5. - (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock; 
  6.  
  7. @end 
當使用 TDD 來開發一個交互器時,是能夠用一個測試用的模擬存儲來代替生產環境的數據存儲的。避免與遠程服務器通信(網絡服務)以及避免讀取磁盤(數據庫)能夠加快你測試的速度並增強其可重複性。
 
將數據存儲保持爲一個界限清晰的特定層的緣由之一是,這可讓你延遲選擇一個特定的持久化技術。若是你的數據存儲是一個獨立的類,那你就可使用一個基礎的持久化策略來開始你的應用,而後等到有意義的時候升級至 SQLite 或者 Core Data。而由於數據存儲層的存在,你的應用代碼庫中就不須要改變任何東西。
 
在 iOS 的項目中使用 Core Data 常常比構架自己還容易引發更多爭議。然而,利用 VIPER 來使用 Core Data 將給你帶來使用 Core Data 的史無前例的良好體驗。在持久化數據的工具層面上,Core Data 能夠保持快速存取和低內存佔用方面,簡直是個神器。可是有個很惱人的地方,它會像觸鬚同樣把 NSManagedObjectContext 延伸至你全部的應用實現文件中,特別是那些它們不應待的地方。VIPER 可使 Core Data 待在正確的地方:數據存儲層。
 
在待辦事項示例中,應用僅有的兩部分知道使用了 Core Data,其一是數據存儲自己,它負責創建 Core Data 堆棧;另外一個是 data manager。Data manager 執行了獲取請求,將數據存儲返回的 NSManagedObject 對象轉換爲標準的 PONSO 模型對象,並傳輸回業務邏輯層。這樣一來,應用程序核心將再也不依賴於 Core Data,附加獲得的好處是,你也不再用擔憂過時數據 (stale) 和沒有良好組織的多線程 NSManagedObjects 來糟蹋你的工做成果了。
 
在經過請求訪問 Core Data 存儲時,data manager 中看起來是這樣的:
  1. @implementation VTDListDataManager 
  2.  
  3. - (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate*)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock 
  4.     NSCalendar *calendar = [NSCalendar autoupdatingCurrentCalendar]; 
  5.  
  6.     NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(date >= %@) AND (date <= %@)", [calendar dateForBeginningOfDay:startDate], [calendar dateForEndOfDay:endDate]]; 
  7.     NSArray *sortDescriptors = @[]; 
  8.  
  9.     __weak typeof(self) welf = self; 
  10.     [self.dataStore 
  11.      fetchEntriesWithPredicate:predicate 
  12.      sortDescriptors:sortDescriptors 
  13.      completionBlock:^(NSArray* entries) { 
  14.          if (completionBlock) 
  15.          { 
  16.              completionBlock([welf todoItemsFromDataStoreEntries:entries]); 
  17.          } 
  18.      }]; 
  19.  
  20. - (NSArray*)todoItemsFromDataStoreEntries:(NSArray *)entries 
  21.     return [entries arrayFromObjectsCollectedWithBlock:^id(VTDManagedTodoItem *todo) { 
  22.         return [VTDTodoItem todoItemWithDueDate:todo.date name:todo.name]; 
  23.     }]; 
  24.  
  25. @end 
與 Core Data 同樣極富爭議的恐怕就是 UI 故事板了。故事板具備不少有用的功能,若是徹底忽視它將會是一個錯誤。然而,調用故事版所能提供的全部功能來完成 VIPER 的全部目標仍然是很困難的。
 
咱們所能作出的妥協就是選擇不使用 segues 。有時候使用 segues 是有效的,可是使用 segues 的危險性在於它們很難原封不動地保持屏幕之間的分離,以及 UI 和應用邏輯之間的分離。通常來講,若是實現 prepareForSegue 方法是必須的話,咱們就儘可能不去使用 segues。
 
除此以外,故事板是一個實現用戶界面佈局有效方法,特別是在使用自動佈局的時候。咱們選擇在實現待辦事項兩個界面的實例中使用故事板,而且使用這樣的代碼來執行本身的導航操做。
  1. static NSString *ListViewControllerIdentifier = @"VTDListViewController"; 
  2.  
  3. @implementation VTDListWireframe 
  4.  
  5. - (void)presentListInterfaceFromWindow:(UIWindow *)window  
  6.     VTDListViewController *listViewController = [self listViewControllerFromStoryboard]; 
  7.     listViewController.eventHandler = self.listPresenter; 
  8.     self.listPresenter.userInterface = listViewController; 
  9.     self.listViewController = listViewController; 
  10.  
  11.     [self.rootWireframe showRootViewController:listViewController 
  12.                                       inWindow:window]; 
  13.  
  14. - (VTDListViewController *)listViewControllerFromStoryboard  
  15.     UIStoryboard *storyboard = [self mainStoryboard]; 
  16.     VTDListViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:ListViewControllerIdentifier]; 
  17.     return viewController; 
  18.  
  19. - (UIStoryboard *)mainStoryboard  
  20.     UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" 
  21.                                                          bundle:[NSBundle mainBundle]]; 
  22.     return storyboard; 
  23.  
  24. @end 
 
使用 VIPER 構建模塊
通常在使用 VIPER 的時候,你會發現一個屏幕或一組屏幕傾向於聚在一塊兒做爲一個模塊。模塊能夠以多種形式體現,但通常最好把它想成是一種特性。在播客應用中,一個模塊多是音頻播放器或訂閱瀏覽器。然而在咱們的待辦事項應用中,列表和添加事項的屏幕都將做爲單獨的模塊被創建。
 
將你的應用做爲一組模塊來設計有不少好處,其中之一就是模塊能夠有很是明確和定義良好的接口,而且獨立於其餘的模塊。這就使增長或者移除特性變得更加簡單,也使在界面中向用戶展現各類可變模塊變得更加簡單。
 
咱們但願能將待辦事項中各模塊之間分隔更加明確,咱們爲添加模塊定義了兩個協議。一個是模塊接口,它定義了模塊能夠作什麼;另外一個則是模塊的代理,用來描述該模塊作了什麼。例如:
  1. @protocol VTDAddModuleInterface  
  2.  
  3. - (void)cancelAddAction; 
  4. - (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate; 
  5.  
  6. @end 
  7.  
  8.  
  9. @protocol VTDAddModuleDelegate  
  10.  
  11. - (void)addModuleDidCancelAddAction; 
  12. - (void)addModuleDidSaveAddAction; 
  13.  
  14. @end 
由於模塊必需要被展現,才能對用戶產生價值,因此模塊的展現器一般須要實現模型的接口。當另外一個模型想要展示當前模塊時,它的展現器就須要實現模型的委託協議,這樣它就能在展現時知道當前模塊作了些什麼。
 
一個模塊可能包括實體,交互器和管理器的通用應用邏輯層,這些一般可用於多個屏幕。固然,這取決於這些屏幕之間的交互及它們的類似度。一個模塊能夠像在待辦事項列表裏面同樣,簡單的只表明一個屏幕。這樣一來,應用邏輯層對於它的特定模塊的行爲來講就很是特有了。
 
模塊一樣是組織代碼的簡便途徑。將模塊全部的編碼都放在它本身的文件夾中並在 Xcode 中建一個 group,這會在你須要尋找和改變動加容易。當你在要尋找一個類時,它恰到好處地就在你所期待的地方,這種感受真是沒法形容的棒。
 
利用 VIPER 創建模塊的另外一個好處是它使得擴展到多平臺時變得更加簡單。獨立在交互器層中的全部用例的應用邏輯容許你能夠專一於爲平板,電話或者 Mac 構建新的用戶界面,同時能夠重用你的應用層。
 
進一步來講,iPad 應用的用戶界面可以將部分 iPhone 應用的視圖,視圖控制器及展現器進行再利用。在這種狀況下,iPad 屏幕將由 ‘super’ 展現器和線框來表明,這樣能夠利用 iPhone 使用過的展現器和線框來組成屏幕。創建進而維護一個跨多平臺的應用是一個巨大的挑戰,可是好的構架能夠對整個模型和應用層的再利用有大幅度的提高,並使其實現起來更加容易。
 
利用 VIPER 進行測試
VIPER 的出現激發了一個關注點的分離,這使得采用 TDD 變得更加簡便。交互器包含獨立與任何 UI 的純粹邏輯,這使測試驅動開發更加簡單。同時展現器包含用來爲顯示準備數據的邏輯,而且它也獨立於任何一個 UIKit 部件。對於這個邏輯的開發也很容易用測試來驅動。
 
咱們更傾向於先從交互器下手。用戶界面裏全部部分都服務於用例,而經過採用 TDD 來測試驅動交互器的 API 可讓你對用戶界面和用例之間的關係有一個更好的瞭解。
 
做爲實例,咱們來看一下負責待辦事項列表的交互器。尋找待辦事項的策略是要找出全部的將在下週末前截止的項目,並將這些項目分別歸類至截止於今天,明天,本週或者下週。
 
咱們編寫的第一個測試是爲了保證交互器可以找到全部的截止於下週末的待辦事項:
  1. - (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek 
  2.     [[self.dataManager expect] todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY]; 
  3.     [self.interactor findUpcomingItems]; 
一旦知道了交互器找到了正確的待辦事項後,咱們就須要編寫幾個小測試用來確認它確實將待辦事項分配到了正確的相對日期組內(好比說今天,明天,等等)。
  1. - (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday 
  2.     NSArray *todoItems = @[[VTDTodoItem todoItemWithDueDate:self.today name:@"Item 1"]]; 
  3.     [self dataStoreWillReturnToDoItems:todoItems]; 
  4.  
  5.     NSArray *upcomingItems = @[[VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationToday dueDate:self.today title:@"Item 1"]]; 
  6.     [self expectUpcomingItems:upcomingItems]; 
  7.  
  8.     [self.interactor findUpcomingItems]; 
既然咱們已經知道了交互器的 API 長什麼樣,接下來就是開發展現器。一旦展現器接收到了交互器傳來的待辦事項,咱們就須要測試看看咱們是否適當的將數據進行格式化而且在用戶界面中正確的顯示它。
  1. - (void)testFoundZeroUpcomingItemsDisplaysNoContentMessage 
  2.     [[self.ui expect] showNoContentMessage]; 
  3.  
  4.     [self.presenter foundUpcomingItems:@[]]; 
  5.  
  6. - (void)testFoundUpcomingItemForTodayDisplaysUpcomingDataWithNoDay 
  7.     VTDUpcomingDisplayData *displayData = [self displayDataWithSectionName:@"Today" 
  8.                                                           sectionImageName:@"check" 
  9.                                                                  itemTitle:@"Get a haircut" 
  10.                                                                 itemDueDay:@""]; 
  11.     [[self.ui expect] showUpcomingDisplayData:displayData]; 
  12.  
  13.     NSCalendar *calendar = [NSCalendar gregorianCalendar]; 
  14.     NSDate *dueDate = [calendar dateWithYear:2014 month:5 day:29]; 
  15.     VTDUpcomingItem *haircut = [VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationToday dueDate:dueDate title:@"Get a haircut"]; 
  16.  
  17.     [self.presenter foundUpcomingItems:@[haircut]]; 
  18.  
  19. - (void)testFoundUpcomingItemForTomorrowDisplaysUpcomingDataWithDay 
  20.     VTDUpcomingDisplayData *displayData = [self displayDataWithSectionName:@"Tomorrow" 
  21.                                                           sectionImageName:@"alarm" 
  22.                                                                  itemTitle:@"Buy groceries" 
  23.                                                                 itemDueDay:@"Thursday"]; 
  24.     [[self.ui expect] showUpcomingDisplayData:displayData]; 
  25.  
  26.     NSCalendar *calendar = [NSCalendar gregorianCalendar]; 
  27.     NSDate *dueDate = [calendar dateWithYear:2014 month:5 day:29]; 
  28.     VTDUpcomingItem *groceries = [VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationTomorrow dueDate:dueDate title:@"Buy groceries"]; 
  29.  
  30.     [self.presenter foundUpcomingItems:@[groceries]]; 
一樣須要測試的是應用是否在用戶想要新建待辦事項時正確啓動了相應操做:
  1. - (void)testAddNewToDoItemActionPresentsAddToDoUI 
  2.     [[self.wireframe expect] presentAddInterface]; 
  3.  
  4.     [self.presenter addNewEntry]; 
這時咱們能夠開發視圖功能了,而且在沒有待辦事項的時候咱們想要展現一個特殊的信息。
  1. - (void)testShowingNoContentMessageShowsNoContentView 
  2.     [self.view showNoContentMessage]; 
  3.  
  4.     XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view"); 
有待辦事項出現時,咱們要確保列表是顯示出來的:
  1. - (void)testShowingUpcomingItemsShowsTableView 
  2.     [self.view showUpcomingDisplayData:nil]; 
  3.  
  4.     XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view"); 
首先創建交互器是一種符合 TDD 的天然規律。若是你首先開發交互器,緊接着是展現器,你就能夠首先創建一個位於這些層的套件測試,而且爲實現這是實例奠基基礎。因爲你不須要爲了測試它們而去與用戶界面進行交互,因此這些類能夠進行快速迭代。在你須要開發視圖的時候,你會有一個能夠工做並測試過的邏輯和表現層來與其進行鏈接。在快要完成對視圖的開發時,你會發現第一次運行程序時全部部件都運行良好,由於你全部已經過的測試已經告訴你它能夠工做。
 
結論
咱們但願你喜歡這篇對 VIPER 的介紹。或許大家都很好奇接下來應該作什麼,若是你但願經過 VIPER 來對你下一個應用進行設計,該從哪裏開始呢?
 
咱們不遺餘力使這篇文章和咱們利用 VIPER 實現的應用實例足夠明確而且進行了很好的定義。咱們的待辦事項裏列表程序至關直接簡單,可是它準確地解釋瞭如何利用 VIPER 來創建一個應用。在實際的項目中,你能夠根據你本身的挑戰和約束條件來決定要如何實踐這個例子。根據以往的經驗,咱們的每一個項目在使用 VIPER 時都或多或少地改變了一些策略,但它們無一例外的都從中得益,找到了正確的方向。
 
不少狀況下因爲某些緣由,你可能會想要偏離 VIPER 所指引的道路。可能你遇到了不少  'bunny' 對象,或者你的應用使用了故事板的 segues。不要緊的,在這些狀況下,你只須要在作決定時稍微考慮下 VIPER 所表明的精神就好。VIPER 的核心在於它是創建在 單一責任原則上的架構。若是你碰到了些許麻煩,想一想這些原則再考慮如何前進。
 
你必定想知道在現有的應用中可否只用 VIPER 。在這種狀況下,你能夠考慮使用 VIPER 構建新的特性。咱們許多現有項目都使用了這個方法。你能夠利用 VIPER 創建一個模塊,這能幫助你發現許多創建在單一責任原則基礎上形成難以運用架構的現有問題。
 
軟件開發最偉大的事情之一就是每一個應用程序都是不一樣的,而設計每一個應用的架構的方式也是不一樣的。這就意味着每一個應用對於咱們來講都是一個學習和嘗試的機遇,若是你決定開始使用 VIPER,你會受益不淺。感謝你的閱讀。
 
Swift 補充
蘋果上週在 WWDC 介紹了一門稱之爲  Swift 的編程語言來做爲 Cocoa 和 Cocoa Touch 開發的將來。如今發表關於 Swift 的完整意見還爲時尚早,但衆所周知編程語言對咱們如何設計和構建應用有着重大影響。咱們決定使用  Swift 重寫咱們的待辦事項清單,幫助咱們學習它對 VIPER 意味着什麼。至今爲止,收穫頗豐。Swift 中的一些特性對於構建應用的體驗有着顯著的提高。
 
結構體
在 VIPER 中咱們使用小型,輕量級的 model 類來在好比從展現器到視圖這樣不一樣的層間傳遞數據。這些 PONSOs 一般是隻是簡單地帶有少許數據,而且一般這些類不會被繼承。Swift 的結構體很是適合這個狀況。下面的結構體的例子來自 VIPER Swift。這個結構體須要被判斷是否相等,因此咱們重載了 == 操做符來比較這個類型的兩個實例。
  1. struct UpcomingDisplayItem : Equatable, Printable { 
  2.     let title : String = "" 
  3.     let dueDate : String = "" 
  4.  
  5.     var description : String { get { 
  6.         return "\(title) -- \(dueDate)" 
  7.     }} 
  8.  
  9.     init(title: String, dueDate: String) { 
  10.         self.title = title 
  11.         self.dueDate = dueDate 
  12.     } 
  13.  
  14. func == (leftSide: UpcomingDisplayItem, rightSide: UpcomingDisplayItem) -> Bool { 
  15.     var hasEqualSections = false 
  16.     hasEqualSections = rightSide.title == leftSide.title 
  17.  
  18.     if hasEqualSections == false { 
  19.         return false 
  20.     } 
  21.  
  22.     hasEqualSections = rightSide.dueDate == rightSide.dueDate 
  23.  
  24.     return hasEqualSections 
 
類型安全
也許 Objective-C 和 Swift 的最大區別是它們在對於類型處理上的不一樣。 Objective-C 是動態類型,而 Swift 故意在編譯時作了嚴格的類型檢查。對於一個相似 VIPER 的架構, 應用由不一樣層構成,類型安全是提高程序員效率和設計架構有很是大的好處。編譯器幫助你確保正確類型的容器和對象在層的邊界傳遞。如上所示,這是一個使用結構體的好地方。若是一個結構體的被設計爲存在於兩層之間,那麼因爲類型安全,你能夠保證它將永遠沒法脫離這些層之間。
相關文章
相關標籤/搜索