用VIPER构建iOS应用
為避免撕逼,提前聲明:本文純屬翻譯,僅僅是為了學習,加上水平有限,見諒!
【原文】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)的首字母組合成的。整潔架構把應用邏輯結構劃分為不同的職責層。這讓依賴分離更加簡單(如:你的數據庫)并且層邊界間的交互也很容易測試。
大多是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的不同組件可以以任何順序在應用中實現,我們選擇按照推薦實現的順序去介紹這些組件。你會發現這個順序和構建整個應用的過程大概一致,首先是討論產品需要做什么,然后用戶如何與它交互。交互器(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]; } 復制代碼應用使用的是自定義視圖控制器轉場去展示“添加”視圖控制器。因為線框圖負責執行轉場動作,所以它成了“添加”視圖控制器的轉場委托,并返回合適的轉場動畫。
適用于VIPER的應用組件
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構建模塊
通常在使用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 = titleself.dueDate = dueDate} } func ==(leftSide: UpcomingDisplayItem, rightSide: UpcomingDisplayItem) -> Bool {var hasEqualSections = falsehasEqualSections = rightSide.title == leftSide.titleif hasEqualSections == false {return false}hasEqualSections = rightSide.dueDate == rightSide.dueDatereturn haseEqualSections } 復制代碼類型安全
也許Object-C和Swift兩者最大的區別是對類型的處理。Object-C是動態類型而Swift對在編譯時實現類型檢查的方式非常嚴格。對于像VIPER這樣的由多個不同層組成的架構來說,類型安全對程序員的效率和總體架構來說是一個巨大的勝利。編譯器幫助你確保容器和對象在層邊界間進行傳遞時類型的正確性。如上面所示,這是使用結構的好地方。如果結構想要在兩層邊界之間生存,多虧了類型安全,你可以保證它將永遠不可能從這兩層間逃離。
總結
以上是生活随笔為你收集整理的用VIPER构建iOS应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python——如何搭建Python的环
- 下一篇: [20180428]DNS与ORA-12