用VIPER構建iOS應用

singmiya發表於2018-04-28

為避免撕逼,提前宣告:本文純屬翻譯,僅僅是為了學習,加上水平有限,見諒!

【原文】https://www.objc.io/issues/13-architecture/singletons/

用VIPER構建iOS應用 ——by Jeff Gilbert and Conrad Stoll

眾所周知,在建築領域,我們塑造我們的建築,隨後我們的建築也塑造我們。正如程式設計師最終知道那樣,這也適用於構建軟體。

設計我們的程式碼很重要,這樣每一個片段都很容易識別,有特定和明確的目的,以合理的方式同其他片段相配合。這就是我們所謂的軟體架構。好的架構不是讓產品成功,而是讓產品可維護並且幫助維護人員保持一個清晰地思路。

在這篇文章中,我們將介紹一種稱之為VIPER的iOS應用架構方案。VIPER已被用來建立了很多大型的專案,但是為了這篇文章的我們通過建立的一個to-do列表應用來向你展示VIPER架構。你可以在GitHub上關注這個示例專案:

視訊

VIPER為何物?

測試不總是構建iOS應用程式的主要部分。當我們開始尋求改善Mutual Mobile的測試實踐時,我們發現為iOS應用寫測試用例很困難。我們決定,如果我們打算改善測試軟體的方式,我們首先要想出一個好的方式來構建應用程式。我們把這種方式稱為VIPER

對iOS程式來說,VIPER是應用整潔架構(Clean Architecture)的架構模式。單詞VIPER是由檢視(View)、互動器(Interactor)、展示器(Presenter)、實體(Entity)和路由(Routing)的首字母組合成的。整潔架構把應用邏輯結構劃分為不同的職責層。這讓依賴分離更加簡單(如:你的資料庫)並且層邊界間的互動也很容易測試。

viper-intro

大多是iOS應用都是使用MVC(model-view-controller)架構的。使用MVC作為應用的架構讓你認為每一個類既是模型(model)也是檢視(view)和控制器(controller)。由於很多應用邏輯都不屬於模型(model)和檢視(view),最後它們都被放在了控制器中。這就導致了一個被稱之為大型檢視控制器(Massive View Controller)的問題,在這裡檢視控制器做了太多的工作。為大型檢視控制器瘦身不單單是尋求改善程式碼質量的iOS程式設計師所面臨的挑戰,它也是一個很好的開始(改善專案的架構的開始)。

VIPER的不同層通過為應用邏輯和導航相關的程式碼提過清晰地位置來應對這一挑戰。隨著VIPER架構的應用,你會意識到在我們的to-do列表例子中的檢視控制器很精簡、很平衡,檢視控制機(view controlling machines)。你也會發現在檢視控制器和其他類中的程式碼很容易理解和測試,因此也更利於維護。

基於用例的應用設計

應用通常作為一組用例來實現。用例也成為驗收標準或者行為,用來描述應用是用來幹嘛的?也許列表需要按時間、型別或者名稱進行排序。這就是個用例。用例是負責業務邏輯的應用層。用例應該獨立於它們的使用者介面實現。它們也應該小且易於定義。決定如何把複雜的應用分解成小巧的用例很有挑戰性而且需要練習,但對於限制你解決的每一個問題和你寫的每一個類的範圍非常有用。

使用VIPER構建應用需要實現一系列元件來完成每一個用例。應用邏輯是每一個用例實現的主要部分,但不是唯一的部分。用例同樣影響著使用者介面。此外,考慮如何讓用例與其他核心元件配合很重要,例如網路和資料展示。元件就像用例的外掛一樣,VIPER描述的是每一個元件等角色是什麼和他們是如何同其他元件互動的。

對於我們的代辦列表應用,其中一個用例或者需求是用使用者選擇的不同的方式組織這些代辦事項。通過把組織資料的邏輯分離成用例,我們可以保持使用者介面程式碼整潔且易於將用例包裝在測試中,以保證它可以如預期的那樣繼續工作。

VIPER的主要部分

VIPER的主要部分是:

  • 檢視(View):顯示展示器讓它顯示的東西並將使用者的輸入傳回給展示器。
  • 互動器(Interactor):包含用例指定的業務邏輯
  • 展示器(Presenter):包含準備展示內容(當從互動器接收到)的邏輯,並對使用者的輸入進行反饋(通過從互動器請求新資料)。
  • 實體(Entity):包含互動器使用的基本的模型物件。
  • 路由(Routing):包含描述哪些介面按照什麼樣的順序戰士的導航邏輯。

這些拆分遵循單一責任原則。互動器(Interactor)負責業務分析,展示器負責互動設計,檢視負責視覺設計。

下面是不同元件的關係圖以及它們是如何連線的:

viper-wireframe
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)應該允許展示器在一個較高的抽象層次上與其通訊,用其內容展示,而不是如何展示內容。展示器不知道UILabelUIButton等的存在。只知道它持有的內容以及該何時展示。如何展示內容這取決於檢視。

檢視是一個定義為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中,路由職責由展示器和線框圖這兩個物件負責。線框圖物件擁有UIWindowUINavigationControllerUIViewController等。它負責穿件檢視/檢視控制器並把它載入到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];
}
複製程式碼

應用使用的是自定義檢視控制器轉場去展示“新增”檢視控制器。因為線框圖負責執行轉場動作,所以它成了“新增”檢視控制器的轉場委託,並返回合適的轉場動畫。

適用於VIPER的應用元件

iOS應用架構需要考慮到一個事實,UIKitCocoa 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是非常好的資料持久化工具,它有著極快的獲取速度和極低的記憶體佔用。但是有一個慣例,就是在應用程式的實現檔案中,即使不應該出現,也需要設定繁瑣的NSManagedObjectContextVIPERCore 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 StoryboardsStoryboards有很多使用的特性,完全的忽略它們是一個錯誤。然而,當使用storyboard提供的所有特性時,很難實現VIPER的所有目標。

常常,我們做的妥協是選擇不使用連線(seguesstoryboardcontroller之間的連線)。可能存在一些使用連線是有意義的例子,使用連線(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構建模組

通常在使用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進行測試

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的方式也不盡相同。對於我們來說,這意味著每一個應用都是一個新的學習和嘗試新鮮東西的機會。

Swift補遺

在上週的蘋果開發者大會上,蘋果介紹了作為未來開發Cocoa和Cocoa Touch的程式語言——Swift。對Swift語言進行深入的點評還為時過早,但是我們知道這個語言對如何設計和構建軟體產生了重大的影響。我們決定使用Swift重寫我們的VIPER待辦示例應用去幫助我們認識這對VIPER意味著什麼。目前為止,我們喜歡我們看到的東西。這裡有幾個我認為可以提高使用VIPER構建應用體驗的Swift特性。

Structs

在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這樣的由多個不同層組成的架構來說,型別安全對程式設計師的效率和總體架構來說是一個巨大的勝利。編譯器幫助你確保容器和物件在層邊界間進行傳遞時型別的正確性。如上面所示,這是使用結構的好地方。如果結構想要在兩層邊界之間生存,多虧了型別安全,你可以保證它將永遠不可能從這兩層間逃離。

相關文章