爲避免撕逼,提早聲明:本文純屬翻譯,僅僅是爲了學習,加上水平有限,見諒!html
【原文】https://www.objc.io/issues/13-architecture/singletons/ios
衆所周知,在建築領域,咱們塑造咱們的建築,隨後咱們的建築也塑造咱們。正如程序員最終知道那樣,這也適用於構建軟件。git
設計咱們的代碼很重要,這樣每個片斷都很容易識別,有特定和明確的目的,以合理的方式同其餘片斷相配合。這就是咱們所謂的軟件架構。好的架構不是讓產品成功,而是讓產品可維護而且幫助維護人員保持一個清晰地思路。程序員
在這篇文章中,咱們將介紹一種稱之爲VIPER的iOS應用架構方案。VIPER
已被用來建立了不少大型的項目,可是爲了這篇文章的咱們經過建立的一個to-do
列表應用來向你展現VIPER
架構。你能夠在GitHub上關注這個示例項目:github
測試不老是構建iOS應用程序的主要部分。當咱們開始尋求改善Mutual Mobile的測試實踐時,咱們發現爲iOS應用寫測試用例很困難。咱們決定,若是咱們打算改善測試軟件的方式,咱們首先要想出一個好的方式來構建應用程序。咱們把這種方式稱爲VIPER
。web
對iOS程序來講,VIPER
是應用整潔架構(Clean Architecture)的架構模式。單詞VIPER
是由視圖(View
)、交互器(Interactor
)、展現器(Presenter
)、實體(Entity
)和路由(Routing
)的首字母組合成的。整潔架構把應用邏輯結構劃分爲不一樣的職責層。這讓依賴分離更加簡單(如:你的數據庫)而且層邊界間的交互也很容易測試。 數據庫
大可能是iOS應用都是使用MVC
(model-view-controller)架構的。使用MVC
做爲應用的架構讓你認爲每個類既是模型(model
)也是視圖(view
)和控制器(controller
)。因爲不少應用邏輯都不屬於模型(model
)和視圖(view
),最後它們都被放在了控制器中。這就致使了一個被稱之爲大型視圖控制器(Massive View Controller)的問題,在這裏視圖控制器作了太多的工做。爲大型視圖控制器瘦身不僅僅是尋求改善代碼質量的iOS程序員所面臨的挑戰,它也是一個很好的開始(改善項目的架構的開始)。編程
VIPER
的不一樣層經過爲應用邏輯和導航相關的代碼提過清晰地位置來應對這一挑戰。隨着VIPER架構的應用,你會意識到在咱們的to-do列表例子中的視圖控制器很精簡、很平衡,視圖控制機(view controlling machines)。你也會發如今視圖控制器和其餘類中的代碼很容易理解和測試,所以也更利於維護。swift
應用一般做爲一組用例來實現。用例也成爲驗收標準或者行爲,用來描述應用是用來幹嗎的?也許列表須要按時間、類型或者名稱進行排序。這就是個用例。用例是負責業務邏輯的應用層。用例應該獨立於它們的用戶界面實現。它們也應該小且易於定義。決定如何把複雜的應用分解成小巧的用例頗有挑戰性並且須要練習,但對於限制你解決的每個問題和你寫的每個類的範圍很是有用。瀏覽器
使用VIPER構建應用須要實現一系列組件來完成每個用例。應用邏輯是每個用例實現的主要部分,但不是惟一的部分。用例一樣影響着用戶界面。此外,考慮如何讓用例與其餘核心組件配合很重要,例如網絡和數據展現。組件就像用例的插件同樣,VIPER描述的是每個組件等角色是什麼和他們是如何同其餘組件交互的。
對於咱們的代辦列表應用,其中一個用例或者需求是用用戶選擇的不一樣的方式組織這些代辦事項。經過把組織數據的邏輯分離成用例,咱們能夠保持用戶界面代碼整潔且易於將用例包裝在測試中,以保證它能夠如預期的那樣繼續工做。
VIPER的主要部分是:
View
):顯示展現器讓它顯示的東西並將用戶的輸入傳回給展現器。Interactor
):包含用例指定的業務邏輯Presenter
):包含準備展現內容(當從交互器接收到)的邏輯,並對用戶的輸入進行反饋(經過從交互器請求新數據)。Entity
):包含交互器使用的基本的模型對象。Routing
):包含描述哪些界面按照什麼樣的順序戰士的導航邏輯。這些拆分遵循單一責任原則。交互器(Interactor
)負責業務分析,展現器負責交互設計,視圖負責視覺設計。
下面是不一樣組件的關係圖以及它們是如何鏈接的:
VIPER的不一樣組件能夠以任何順序在應用中實現,咱們選擇按照推薦實現的順序去介紹這些組件。你會發現這個順序和構建整個應用的過程大概一致,首先是討論產品須要作什麼,而後用戶如何與它交互。Interactor
)交互器表示單個應用用例。它包含操做模型對象(Entities
)的業務邏輯去執行特定的任務。交互器中所作的工做應該獨立於UI。一樣的交互器能夠用在iOS應用中或者OSX應用中。
由於交互器是主要包含邏輯的簡單對象(PONSO:Plain Old NSObject
),因此使用TDD很容易開發。
這個簡單應用的主要用例是展現用戶即將到來的代買事項(例如:下星期到期的任何東西)。這個用例的業務邏輯是查詢出今天和下週末之間到期的任何待辦事項,而後爲其指定一個相關的到期時間:今天,明天,本週晚些時候,下週。
下面是來自VTDListInteractor
的相應方法:
- (vodd)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]];
}];
}
複製代碼
Entity
)實體是由交互器(Interactor
)操做的模型對象。實體(Entity
)只能由交互器(Interactor
)來操做。交互器(Interactor
)毫不會把實體(Entity
)傳遞給展現層(如:展現器(Presenter
))。
實體(Entity
)每每也是普通對象。若是你是用Core Data
,你將會但願你的管理對象保持在數據層以後。交互器不該該同NSManagedObjects
一塊兒使用。
下面是咱們的待辦項實體:
@interface VTDTodoItem: NSObject
@property (nonatomic, strong) NSDate *dueDate;
@property (nonatomic, copy) NSString *name;
+ (instancetype)todoItemWithDueDate:(NSDate *)dueDate name:(NSString *)name;
@end
複製代碼
若是你的實體僅僅只是數據結構請不要大驚小怪。任何應用相關的邏輯大多數都在交互器中。
Presenter
)展現器是主要包含驅動UI邏輯的普通對象。它知道什麼時候展現用戶界面。它從用戶交互中獲取輸入,因此它能夠更新UI並向交互器發送請求。
當用戶點擊「+」按鈕添加新代辦事項時,addNewEntry
就被調用了。對於這個方法,展現器要求線框展現用於添加新項的UI:
- (void)addNewEntry {
[self.listWireframe presentAddInterface];
}
複製代碼
展現器也接收來自交互器的結果,並把結果轉換爲能夠在視圖中高校展現的表單。
下面是從交互器接收即將到來項目的方法。它會處理數據並決定向用戶展現哪些東西:
- (void)foundUpcomingItems:(NSArray *)upcomingItems {
if([upcomingItems count] == 0) {
[self.userInterface showNoContentMessage];
} else {
[self updateUserInterfaceWithUpcomingItems:upcomingItems];
}
}
複製代碼
毫不會把實體從交互器傳遞到展現器。而是把簡單沒有行爲的數據結構從交互器傳到了展現器。這能夠防止在展現器中完成任何「實際工做」。展現器只爲視圖準備展現的數據。
View
)視圖是被動的。它等待展現器給它展現的內容;從不主動向展現器請求數據。爲視圖定義的方法(如:登錄界面的LoginView
)應該容許展現器在一個較高的抽象層次上與其通訊,用其內容展現,而不是如何展現內容。展現器不知道UILabel
、UIButton
等的存在。只知道它持有的內容以及該什麼時候展現。如何展現內容這取決於視圖。
視圖是一個定義爲Objective—C協議的抽象接口。一個視圖控制器(UIViewController
)或者其子類將會實現這個視圖協議。例如,咱們的示例中的添加界面有以下接口:
@protocol VTDAddViewInterface <NSObject>
- (void)setEntryName:(NSString *)name;
- (void)setEntryDueDate:(NSDate *)date;
複製代碼
視圖和視圖控制器都處理用戶交互和輸入。這也就不難理解爲何視圖控制器老是會變得那麼臃腫,由於這裏是最容易處理該輸入去執行一些動做的地方。爲了讓視圖控制器保證精簡,當用戶執行肯定的動做時咱們須要提供一種方式去通知對其感興趣的部分。視圖控制器不能基於這些動做作出決定,可是能夠把這些事件傳遞到能夠作決定的地方。
在咱們的例子中,「添加」視圖控制器具備符合下面接口的事件處理器屬性:
@protocol VTDAddModuleInterface <NSObject>
- (void)cancelAddAction;
- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;
@end
複製代碼
當用戶點擊取消按鈕,視圖控制器告訴用戶指定的事件處理器它去次奧了添加動做。那樣,事件處理器能夠作出以下處理:關閉「添加」視圖控制器和通知列表視圖更新。
視圖和展現器之間的邊界是使用ReactiveCocoa的絕佳地方。在這個例子中,視圖控制器能夠提供方法返回表明按鈕動做的信號。這可讓展現器很容易的對這些信號進行響應,而不用破壞職責分離。
Routing
)由交互設計師設計的線框圖定義了從一個界面到另外一個界面的路由。在VIPER
中,路由職責由展現器和線框圖這兩個對象負責。線框圖對象擁有UIWindow
、UINavigationController
、UIViewController
等。它負責穿件視圖/視圖控制器並把它加載到window上。
因爲展現器包含響應用戶輸入的邏輯,因此展現器知道什麼時候導航到其餘的界面以及導航到哪一個界面。固然,線框圖也知道如何導航。所以,展現器將使用線框圖執行導航。他們共同描述了一個從一個視圖導航到下一個的路由。
線框圖也是一個明顯的處理導航轉場動畫的地方。看一下來自於"添加"線框圖的例子:
@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;
}
@end
#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];
}
複製代碼
應用使用的是自定義視圖控制器轉場去展現「添加」視圖控制器。由於線框圖負責執行轉場動做,因此它成了「添加」視圖控制器的轉場委託,並返回合適的轉場動畫。
iOS應用架構須要考慮到一個事實,UIKit
和Cocoa Touch
是構建應用的主要工具。架構須要同應用中全部的組件和諧共處,可是,這也須要提供參考指南,用來講明框架中的一些模塊如何使用以及用在何處。
iOS應用的主力是UIViewController
。咱們很容易認爲,取代MVC
的競爭者能夠避免視圖控制器的過分使用。但,視圖控制器是平臺的核心:它們處理屏幕翻轉,響應用戶輸入,與像導航控制器這樣的系統組件組合,如今在iOS7中,也許自定義界面轉場動做。很是有用。
使用VIPER,視圖控制器執行它應該作的事情:控制視圖。咱們的代辦列表應用有兩個視圖控制器,一個是列表界面,另外一個是「添加」界面。「添加」視圖控制制器的實現很基礎,由於它所要作的就是控制視圖:
@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
客戶端的依賴。交互器可能須要從多個數據源彙總數據,以提供完成用例所需的信息。而後由展現器接收由交互器返回的數據,併爲展現進行格式化。
數據存儲負責向交互器提供實體。因爲交互器應用其交互邏輯,它須要從數據存儲取回實體,處理實體並把更新過的實體放回到數據存儲中。數據存儲管理持久化的實體。實體不知道數據存儲,所以也就不知道如何對本身進行持久化。
交互器也不該該知道如何持久化實體。有時,交互器可能須要使用一個被稱爲數據管理器的對象去幫助本身同數據存儲進行交互。數據管理器處理特定存儲類型的操做,像建立獲取數據請求,建立查詢等。這讓交互器更多的關注應用邏輯而不用知道實體是如何獲取和持久化實體的。在你使用Core Data
的時候使用數據管理器纔是有意義的,你能夠在下面看到對他的描述。
這是示例應有的數據管理器接口:
@interface VTDListDataManager: NSObject
@property (nonatomic, strong) VTDCoreDataStore *dataStore;
- (void)todoItemsBetweenStartDate:(NSData *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;
@end
複製代碼
可是用TDD
開發交互器時,可使用測試double/mock
來切換生產數據存儲。不與遠程服務器(用於web服務)和本地磁盤(用於數據庫)進行通訊可讓你的測試更快速且更可重複。
把數據存儲放在邊界明顯的層的理由是,它容許你推遲選擇特定的持久化技術。若是你的數據存儲是單個類,你可使用使用基本的持久策略啓動你的應用,而後在適當的狀況下升級到到SQLite
或者Core Date
,而無需更改應用代碼庫中的其餘任何內容。
在iOS項目中使用Core Date
常常會引起比架構本身還要多的爭議。然而,在VIPER
中使用Core Date
能夠成爲你曾經有過的最好的Core Date
使用體驗。Core Date
是很是好的數據持久化工具,它有着極快的獲取速度和極低的內存佔用。可是有一個慣例,就是在應用程序的實現文件中,即便不該該出現,也須要設置繁瑣的NSManagedObjectContext
。VIPER
把Core Data
放在了它應該在的地方:數據存儲層。
在待辦列表例子中,應用僅有的兩個部分知道Core Data
正在被使用的是數據存儲自己,在這裏設置Core Data
堆棧和數據管理器。數據管理器執行獲取請求,把數據存儲層返回的NSManagedObjects對象轉換成標準的簡單對象模型,並把它返回給業務邏輯層。這樣,應用程序的核心就不會依賴Core Data
,做爲回報,你不用擔憂因爲過期或線程有問題的NSManagedObjects
而致使應用沒法工做。
在數據管理器中,當請求訪問Core Data
存儲時,看起來是下面這樣:
@implementatin 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 type(self) welf = self;
[self.dateStore fetchEntriesWithPredicate:predicate sortDescriptors:sortDescriptors completionBlock:^(NSArray *entries){
if(completionBlock) {
completionBlock([welf todoItemsFromDataStoreEntries:entries]);
}
}];
}
- (NSArray *)todoItemsFromDataStoreEntries:(NSArray *)entries {
return [entries arrayFromObjectsCollectedWithBlock:^id(VTDManagedTodoItems *todo) {
return [VTDTodoItem todoItemWithDueDate:todo.date name:todo.name];
}];
}
@end
複製代碼
幾乎同Core Data
有一樣爭議的是UI Storyboards
。Storyboards
有不少使用的特性,徹底的忽略它們是一個錯誤。然而,當使用storyboard
提供的全部特性時,很難實現VIPER的全部目標。
經常,咱們作的妥協是選擇不使用連線(segues
:storyboard
中controller
之間的連線)。可能存在一些使用連線是有意義的例子,使用連線(segues
)的危險在於,很難保持界面之間、UI和應用邏輯之間的完整分離。通常來講,當明顯須要實現prepareForSegue
方法的時候,咱們儘可能不要使用連線(segues
)。
此外,storyboards
是一種很好的實現用戶界面佈局的方式,特別是在使用自動佈局的時候(Auto Layout
)。待辦列表例子中的兩個界面咱們都是用storyboard
來實現,而後用以下代碼去執行咱們本身的導航:
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的時候,你會發現一個界面或一組界面經常會做爲一個模塊組織在一塊兒。一個模塊能夠有幾種方式描述,一般的把它做爲一個特性來描述是最好的選擇。在一個播客應用中,模塊多是一個音頻播放器或者訂閱瀏覽器。在咱們的待辦列表應用中,列表和「添加」界面都構建成了獨立的模塊。
把你的應用設計成一系列模塊有幾個好處。其中一個是:模塊有着清晰且定義良好的接口,同時獨立於其餘模塊。這使得添加/移除特性或者改變你的接口向用戶呈現各類模塊的方式。
咱們但願在待辦列表例子中清晰的區分模塊,因此咱們爲「添加」模塊定義了兩個協議。第一個是模塊接口,這裏定義了模塊能夠作什麼。第二個是模塊委託,這裏描述模塊作了什麼。例如:
@protocol VTDAddModuleInterface <NSObject>
- (void)cancelAddAction;
- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;
@end
@protocol VTDAddModuleDelegate <NSObject>
- (void)addModuleDidCancelAddAction;
- (void)addModelDidSaveAddAction;
@end
複製代碼
因爲模塊必須得呈現給用戶,因此模塊一般會實現模塊接口。當另一個接口想展現這個模塊時,他的展現器須要實現模塊接口協議,這樣它就能夠知道在展現它時模塊作了什麼。
模塊可能包含用於多個界面的實體、交互器和管理器的通用應用邏輯層。固然,這依賴於這些界面之間的交互和他們之間的類似度。一個模塊能夠很容易的表明一個界面,正如在待辦列表示例中多展現的那樣。這種狀況下,應用邏輯層能夠對應於特定模塊中很是具體的行爲。
模塊也是一種很好的組織代碼方式。把一個模塊的代碼隱藏在本身的文件夾內而且Xcode
中組會讓很容易的找到你須要修改的東西。當你在指望的地方找到一個類時,這是一種很棒的感受。
使用VIPER構建模塊的另外一個好處是它們很容易擴展到多種形式。在交互層分離全部用例的應用邏輯讓你在重用應用層的同時還專一於爲平板電腦、手機、和mac電腦構建新的用戶界面。
更進一步,iPad應用的用戶界面可能會重用iPhone應用的一些視圖、視圖控制器和展現器。這種狀況下,一個iPad界面可能會由父展現器和線框圖所表明,它可能會使用已存在的iPhone展現器和線框圖組成界面。構建和維護跨平臺的應用會至關有挑戰性,可是能在整個應用和應用層促進重用的良好架構可讓這變的更容易。
VIPER鼓勵分離關注點這使得它更容易適應TDD。交互器包含獨立於UI的純邏輯,這使得測試更容易驅動。展現器包含爲展現準備數據的邏輯且它獨立於任何UIKit
控件。開發這個邏輯也讓測試更易驅動。
咱們首選的方法從交互器開始。UI中的全部內容均可以知足用例的須要。經過使用TDD爲交互器的API去測試驅動,你會更好的理解UI和用例之間的關係。
例如,咱們將看到負責即將到來的待辦事項列表的交互器。尋找即將到來項的規則是查詢出截止到下週結束的全部待辦事項並按照截止到今天、明天、本週晚些時候或者下週對每一個待辦項進行分類。
咱們寫的第一個例子是保證交互器找出截止到下週結束的全部待辦事項:
- (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長什麼樣了,咱們能夠開發展現器了。當展現器接收到來自交互器的即將到來的待辦事項時,咱們將要測試咱們是否正確的格式化數據並把它顯示在UI上:
- (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];
NSData *dueData = [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制定的路線的狀況。也許你會碰見一個「兔子」對象,或者你的應用會在Storyboard中使用連線(segues
)受益。不要緊,在這些狀況下,當你作決定的時候,想一下VIPER所表明的思想。VIPER的核心是一個基於單一責任原則的架構。當在決定如何繼續下一步的時候,若是你有疑問能夠想一下這個原則。
你可能想知道,若是在已存在的應用中使用VIPER是否可行。在這種狀況下,能夠考慮構建使用VIEPR構建一個新特性。不少咱們已存在的項目均可以採起這種方式。這容許你使用VIPER構建一個模塊,而且能夠幫助你發現任何已存在的問題,這是這個問題讓你很難適應基於單一責任原則的架構。
每個應用都有所差別這是開發軟件最重要的事情之一,而且構建app的方式也不盡相同。對於咱們來講,這意味着每個應用都是一個新的學習和嘗試新鮮東西的機會。
在上週的蘋果開發者大會上,蘋果介紹了做爲將來開發Cocoa和Cocoa Touch的編程語言——Swift。對Swift語言進行深刻的點評還爲時過早,可是咱們知道這個語言對如何設計和構建軟件產生了重大的影響。咱們決定使用Swift重寫咱們的VIPER待辦示例應用去幫助咱們認識這對VIPER意味着什麼。目前爲止,咱們喜歡咱們看到的東西。這裏有幾個我認爲能夠提升使用VIPER構建應用體驗的Swift特性。
在VIPER中咱們使用小且輕量級的模型類在層之間傳遞數據,如:從展現器到視圖。這些普通對象一般只是想簡單地攜帶少許的數據,並不想被子類化。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 haseEqualSections
}
複製代碼
也許Object-C和Swift二者最大的區別是對類型的處理。Object-C是動態類型而Swift對在編譯時實現類型檢查的方式很是嚴格。對於像VIPER這樣的由多個不一樣層組成的架構來講,類型安全對程序員的效率和整體架構來講是一個巨大的勝利。編譯器幫助你確保容器和對象在層邊界間進行傳遞時類型的正確性。如上面所示,這是使用結構的好地方。若是結構想要在兩層邊界之間生存,多虧了類型安全,你能夠保證它將永遠不可能從這兩層間逃離。