使用VIPER構(gòu)建iOS應(yīng)用
2014-07-03 09:53 編輯: qiancheng 分類:iOS開(kāi)發(fā) 來(lái)源:CocoaChina
轉(zhuǎn)自Di Wu's blog,原文:Architecting iOS Apps with VIPER
建筑領(lǐng)域流行這樣一句話梆砸,“我們雖然在營(yíng)造建筑年柠,但建筑也會(huì)重新塑造我們”扳碍。正如所有開(kāi)發(fā)者最終領(lǐng)悟到的,這句話同樣適用于構(gòu)建軟件黔宛。
編寫(xiě)代碼中至關(guān)重要的是,需要使每一部分容易被識(shí)別,賦有一個(gè)特定而明顯的目的刮刑,并與其他部分在邏輯關(guān)系中完美契合。這就是我們所說(shuō)的軟件架構(gòu)养渴。好的架構(gòu)不僅讓一個(gè)產(chǎn)品成功投入使用雷绢,還可以讓產(chǎn)品具有可維護(hù)性,并讓人不斷頭腦清醒的對(duì)它進(jìn)行維護(hù)理卑!
在這篇文章中翘紊,我們介紹了一種稱之為 VIPER 的 iOS 應(yīng)用架構(gòu)的方式。VIPER 已經(jīng)在很多大型的項(xiàng)目上成功實(shí)踐藐唠,但是出于本文的目的我們將通過(guò)一個(gè)待辦事項(xiàng)清單 (to-do app) 來(lái)介紹 VIPER 帆疟。你可以在 GitHub 上關(guān)注這個(gè)項(xiàng)目鹉究。
什么是 VIPER?
測(cè)試永遠(yuǎn)不是構(gòu)建 iOS 應(yīng)用的主要部分踪宠。當(dāng)我們 (Mutual Mobile) 著手改善我們的測(cè)試實(shí)踐時(shí)自赔,我們發(fā)現(xiàn)給 iOS 應(yīng)用寫(xiě)測(cè)試代碼非常困難。因此如果想要設(shè)法改變測(cè)試的現(xiàn)狀柳琢,我們首先需要一個(gè)更好的方式來(lái)架構(gòu)應(yīng)用绍妨,我們稱之為 VIPER。
VIPER 是一個(gè)創(chuàng)建 iOS 應(yīng)用簡(jiǎn)明構(gòu)架的程序柬脸。VIPER 可以是視圖 (View)他去,交互器 (Interactor),展示器 (Presenter)肖粮,實(shí)體 (Entity) 以及路由 (Routing) 的首字母縮寫(xiě)孤页。簡(jiǎn)明架構(gòu)將一個(gè)應(yīng)用程序的邏輯結(jié)構(gòu)劃分為不同的責(zé)任層。這使得它更容易隔離依賴項(xiàng) (如數(shù)據(jù)庫(kù))涩馆,也更容易測(cè)試各層間的邊界處的交互:
大部分 iOS 應(yīng)用利用 MVC 構(gòu)建行施,使用 MVC 應(yīng)用程序架構(gòu)可以引導(dǎo)你將每一個(gè)類看做模型,視圖或控制器中的一個(gè)魂那。但由于大部分應(yīng)用程序的邏輯不會(huì)存在于模型或視圖中蛾号,所以通常最終總是在控制器里實(shí)現(xiàn)。這就導(dǎo)致一個(gè)稱為重量級(jí)視圖控制器的問(wèn)題涯雅,在這里鲜结,視圖控制器做了太多工作。為這些重量級(jí)視圖控制器瘦身并不是 iOS 開(kāi)發(fā)者尋求提高代碼的質(zhì)量所要面臨的唯一挑戰(zhàn)活逆,但至少這是一個(gè)很好的開(kāi)端钧嘶。
VIPER 的不同層提供了明確的程序邏輯以及導(dǎo)航控制代碼來(lái)應(yīng)對(duì)這個(gè)挑戰(zhàn)舀寓,利用 VIPER 败许,你會(huì)注意到在我們的待辦事項(xiàng)示例清單中的視圖控制器可以簡(jiǎn)潔高效柑潦,意義明確地控制視圖。你也會(huì)發(fā)現(xiàn)視圖控制器中代碼和所有的其他類很容易理解锈遥,容易測(cè)試纫事,理所當(dāng)然也更易維護(hù)。
基于用例的應(yīng)用設(shè)計(jì)
應(yīng)用通常是一些用戶用例的集合所灸。用例也被稱為驗(yàn)收標(biāo)準(zhǔn)丽惶,或行為集,它們用來(lái)描述應(yīng)用的用途爬立。清單可以根據(jù)時(shí)間钾唬,類型以及名字排序,這就是一個(gè)用例。用例是應(yīng)用程序中用來(lái)負(fù)責(zé)業(yè)務(wù)邏輯的一層知纷,應(yīng)獨(dú)立于用戶界面的實(shí)現(xiàn)壤圃,同時(shí)要足夠小陵霉,并且有良好的定義琅轧。決定如何將一個(gè)復(fù)雜的應(yīng)用分解成較小的用例非常具有挑戰(zhàn)性,并且需要長(zhǎng)期實(shí)踐踊挠,但這對(duì)于縮小你解決的問(wèn)題時(shí)所要面臨的范圍及完成的每個(gè)類的所要涉及的內(nèi)容來(lái)說(shuō)乍桂,是很有幫助的。
利用 VIPER 建立一個(gè)應(yīng)用需要實(shí)施一組套件來(lái)滿足所有的用例效床,應(yīng)用邏輯是實(shí)現(xiàn)用例的主要組成部分睹酌,但卻不是唯一。用例也會(huì)影響用戶界面剩檀。另一個(gè)重要的方面憋沿,是要考慮用例如何與其他應(yīng)用程序的核心組件相互配合,例如網(wǎng)絡(luò)和數(shù)據(jù)持久化沪猴。組件就好比用例的插件辐啄,VIPER 則用來(lái)描述這些組件的作用是什么,如何進(jìn)行交互运嗜。
我們其中一個(gè)用例壶辜,或者說(shuō)待辦事項(xiàng)清單中其中的一個(gè)需求是可以基于用戶的選擇來(lái)將待辦事項(xiàng)分組。通過(guò)分離的邏輯將數(shù)據(jù)組織成一個(gè)用例担租,我們能夠在測(cè)試時(shí)使用戶界面代碼保持干凈砸民,用例更易組裝,從而確保它如我們預(yù)期的方式工作奋救。
VIPER 的主要部分
VIPER 的主要部分是:
視圖:根據(jù)展示器的要求顯示界面岭参,并將用戶輸入反饋給展示器。
交互器:包含由用例指定的業(yè)務(wù)邏輯尝艘。
展示器:包含為顯示(從交互器接受的內(nèi)容)做的準(zhǔn)備工作的相關(guān)視圖邏輯演侯,并對(duì)用戶輸入進(jìn)行反饋(從交互器獲取新數(shù)據(jù))。
實(shí)體:包含交互器要使用的基本模型對(duì)象利耍。
路由:包含用來(lái)描述屏幕顯示和顯示順序的導(dǎo)航邏輯蚌本。
這種分隔形式同樣遵循單一責(zé)任原則。交互器負(fù)責(zé)業(yè)務(wù)分析的部分隘梨,展示器代表交互設(shè)計(jì)師程癌,而視圖相當(dāng)于視覺(jué)設(shè)計(jì)師。
以下則是不同組件的相關(guān)圖解轴猎,并展示了他們之間是如何關(guān)聯(lián)的:
雖然在應(yīng)用中 VIPER 的組件可以以任意順序?qū)崿F(xiàn)嵌莉,我們?cè)谶@里選擇按照我們推薦的順序來(lái)進(jìn)行介紹。你會(huì)注意到這個(gè)順序與構(gòu)建整個(gè)應(yīng)用的進(jìn)程大致符合 -- 首先要討論的是產(chǎn)品需要做什么捻脖,以及用戶會(huì)如何與之交互锐峭。
交互器
交互器在應(yīng)用中代表著一個(gè)獨(dú)立的用例中鼠。它具有業(yè)務(wù)邏輯以操縱模型對(duì)象(實(shí)體)執(zhí)行特定的任務(wù)。交互器中的工作應(yīng)當(dāng)獨(dú)立與任何用戶界面沿癞,同樣的交互器可以同時(shí)運(yùn)用于 iOS 應(yīng)用或者 OS X 應(yīng)用中援雇。
由于交互器是一個(gè) PONSO (Plain Old NSObject,普通的 NSObject)椎扬,它主要包含了邏輯惫搏,因此很容易使用 TDD 進(jìn)行開(kāi)發(fā)。
示例應(yīng)用的主要用例是向用戶展示所有的待辦事項(xiàng)(比如任何截止于下周末的任務(wù))蚕涤。此類用例的業(yè)務(wù)邏輯主要是找出今天至下周末之間將要到期的待辦事項(xiàng)筐赔,然后為它們分配一個(gè)相對(duì)的截止日期,比如今天揖铜,明天茴丰,本周以內(nèi),或者下周天吓。
以下是來(lái)自 VTDListInteractor 的對(duì)應(yīng)方法:
1. - (void)findUpcomingItems
2. {
3. __weak typeof(self) welf = self;
4. NSDate* today = [self.clock today];
5. NSDate* endOfNextWeek = [[NSCalendar currentCalendar] dateForEndOfFollowingWeekWithDate:today];
6. [self.dataManager todoItemsBetweenStartDate:today endDate:endOfNextWeek completionBlock:^(NSArray* todoItems) {
7. [welf.output foundUpcomingItems:[welf upcomingItemsFromToDoItems:todoItems]];
8. }];
9. }
實(shí)體
實(shí)體是被交互器操作的模型對(duì)象贿肩,并且它們只被交互器所操作。交互器永遠(yuǎn)不會(huì)傳輸實(shí)體至表現(xiàn)層 (比如說(shuō)展示器)失仁。
實(shí)體也應(yīng)該是 PONSOs尸曼。如果你使用 Core Data,最好是將托管對(duì)象保持在你的數(shù)據(jù)層之后萄焦,交互器不應(yīng)與 NSManageObjects 協(xié)同工作控轿。
這里是我們的待辦事項(xiàng)服務(wù)的實(shí)體:
1. @interface VTDTodoItem : NSObject
3. @property (nonatomic, strong) NSDate* dueDate;
4. @property (nonatomic, copy) NSString* name;
6. + (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;
8. @end
不要詫異于你的實(shí)體僅僅是數(shù)據(jù)結(jié)構(gòu),任何依賴于應(yīng)用的邏輯都應(yīng)該放到交互器中拂封。
展示器
展示器是一個(gè)主要包含了驅(qū)動(dòng)用戶界面的邏輯的 PONSO茬射,它總是知道何時(shí)呈現(xiàn)用戶界面∶扒基于其收集來(lái)自用戶交互的輸入功能在抛,它可以在合適的時(shí)候更新用戶界面并向交互器發(fā)送請(qǐng)求。
當(dāng)用戶點(diǎn)擊 “+” 鍵新建待辦事項(xiàng)時(shí)萧恕,addNewEntry 被調(diào)用刚梭。對(duì)于此項(xiàng)操作,展示器會(huì)要求 wireframe 顯示用戶界面以增加新項(xiàng)目:
1. - (void)addNewEntry
2. {
3. [self.listWireframe presentAddInterface];
4. }
展示器還會(huì)從交互器接收結(jié)果并將結(jié)果轉(zhuǎn)換成能夠在視圖中有效顯示的形式票唆。
下面是如何從交互器接受待辦事項(xiàng)的過(guò)程朴读,其中包含了處理數(shù)據(jù)的過(guò)程并決定展現(xiàn)給用戶哪些內(nèi)容:
1. - (void)foundUpcomingItems:(NSArray*)upcomingItems
2. {
3. if ([upcomingItems count] == 0)
4. {
5. [self.userInterface showNoContentMessage];
6. }
7. else
8. {
9. [self updateUserInterfaceWithUpcomingItems:upcomingItems];
10. }
11. }
實(shí)體永遠(yuǎn)不會(huì)由交互器傳輸給展示器,取而代之走趋,那些無(wú)行為的簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu)會(huì)從交互器傳輸?shù)秸故酒髂抢镄平稹_@就防止了那些“真正的工作”在展示器那里進(jìn)行,展示器只能負(fù)責(zé)準(zhǔn)備那些在視圖里顯示的數(shù)據(jù)。
視圖
視圖一般是被動(dòng)的氮唯,它通常等待展示器下發(fā)需要顯示的內(nèi)容鉴吹,而不會(huì)向其索取數(shù)據(jù)。視圖(例如登錄界面的登錄視圖控件)所定義的方法應(yīng)該允許展示器在高度抽象的層次與之交流惩琉。展示器通過(guò)內(nèi)容進(jìn)行表達(dá)豆励,而不關(guān)心那些內(nèi)容所顯示的樣子。展示器不知道 UILabel琳水,UIButton 等的存在肆糕,它只知道其中包含的內(nèi)容以及何時(shí)需要顯示般堆。內(nèi)容如何被顯示是由視圖來(lái)進(jìn)行控制的在孝。
視圖是一個(gè)抽象的接口 (Interface),在 Objective-C 中使用協(xié)議被定義淮摔。一個(gè) UIViewController 或者它的一個(gè)子類會(huì)實(shí)現(xiàn)視圖協(xié)議私沮。比如我們的示例中 “添加” 界面會(huì)有以下接口:
1. @protocol VTDAddViewInterface
3. - (void)setEntryName:(NSString *)name;
4. - (void)setEntryDueDate:(NSDate *)date;
6. @end
視圖和視圖控制器同樣會(huì)操縱用戶界面和相關(guān)輸入。因?yàn)橥ǔ?lái)說(shuō)視圖控制器是最容易處理這些輸入和執(zhí)行某些操作的地方和橙,所以也就不難理解為什么視圖控制器總是這么大了仔燕。為了使視圖控制器保持苗條,我們需要使它們?cè)谟脩暨M(jìn)行相關(guān)操作的時(shí)候可以有途徑來(lái)通知相關(guān)部分魔招。視圖控制器不應(yīng)當(dāng)根據(jù)這些行為進(jìn)行相關(guān)決定晰搀,但是它應(yīng)當(dāng)將發(fā)生的事件傳遞到能夠做決定的部分。
在我們的例子中办斑,Add View Controller 有一個(gè)事件處理的屬性外恕,它實(shí)現(xiàn)了如下接口:
1. @protocol VTDAddModuleInterface
3. - (void)cancelAddAction;
4. - (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate
6. @end
當(dāng)用戶點(diǎn)擊取消鍵的時(shí)候,視圖控制器告知這個(gè)事件處理程序用戶需要其取消這次添加的動(dòng)作乡翅。這樣一來(lái)鳞疲,事件處理程序便可以處理關(guān)閉 add view controller 并告知列表視圖進(jìn)行更新。
視圖和展示器之間邊界處是一個(gè)使用 ReactiveCocoa 的好地方蠕蚜。在這個(gè)示例中尚洽,視圖控制器可以返回一個(gè)代表按鈕操作的信號(hào)。這將允許展示器在不打破職責(zé)分離的前提下輕松地對(duì)那些信號(hào)進(jìn)行響應(yīng)靶累。
路由
屏幕間的路徑會(huì)在交互設(shè)計(jì)師創(chuàng)建的線框 (wireframes) 里進(jìn)行定義腺毫。在 VIPER 中,路由是由兩個(gè)部分來(lái)負(fù)責(zé)的:展示器和線框挣柬。一個(gè)線框?qū)ο蟀?UIWindow潮酒,UINavigationController,UIViewController 等部分凛忿,它負(fù)責(zé)創(chuàng)建視圖/視圖控制器并將其裝配到窗口中澈灼。
由于展示器包含了響應(yīng)用戶輸入的邏輯,因此它就擁有知曉何時(shí)導(dǎo)航至另一個(gè)屏幕以及具體是哪一個(gè)屏幕的能力。而同時(shí)叁熔,線框知道如何進(jìn)行導(dǎo)航委乌。在兩者結(jié)合起來(lái)的情況下,展示器可以使用線框來(lái)進(jìn)行實(shí)現(xiàn)導(dǎo)航功能荣回,它們兩者一起描述了從一個(gè)屏幕至另一個(gè)屏幕的路由過(guò)程遭贸。
線框同時(shí)也明顯是一個(gè)處理導(dǎo)航轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的地方。來(lái)看看這個(gè) add wireframe 中的例子吧:
1. @implementation VTDAddWireframe
3. - (void)presentAddInterfaceFromViewController:(UIViewController *)viewController
4. {
5. VTDAddViewController *addViewController = [self addViewController];
6. addViewController.eventHandler = self.addPresenter;
7. addViewController.modalPresentationStyle = UIModalPresentationCustom;
8. addViewController.transitioningDelegate = self;
10. [viewController presentViewController:addViewController animated:YES completion:nil];
12. self.presentedViewController = viewController;
13. }
15. #pragma mark - UIViewControllerTransitioningDelegate Methods
17. - (id<uiviewcontrolleranimatedtransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed </uiviewcontrolleranimatedtransitioning>
18. {
19. return [[VTDAddDismissalTransition alloc] init];
20. }
22. - (id<uiviewcontrolleranimatedtransitioning>)animationControllerForPresentedController:(UIViewController *)presented </uiviewcontrolleranimatedtransitioning>
23. presentingController:(UIViewController *)presenting
24. sourceController:(UIViewController *)source
25. {
26. return [[VTDAddPresentationTransition alloc] init];
27. }
29. @end
應(yīng)用使用了自定義的視圖控制器轉(zhuǎn)場(chǎng)來(lái)呈現(xiàn) add view controller心软。因?yàn)榫€框部件負(fù)責(zé)實(shí)施這個(gè)轉(zhuǎn)場(chǎng)壕吹,所以它成為了 add view controller 轉(zhuǎn)場(chǎng)的委托,并且返回適當(dāng)?shù)霓D(zhuǎn)場(chǎng)動(dòng)畫(huà)删铃。
利用 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咒劲,我們不難想象找一個(gè)競(jìng)爭(zhēng)者來(lái)取代 MVC 就可以避免大量使用視圖控制器。但是視圖控制器現(xiàn)在是這個(gè)平臺(tái)的核心:它們處理設(shè)備方向的變化诫隅,回應(yīng)用戶的輸入腐魂,和類似導(dǎo)航控制器之類的系統(tǒng)系統(tǒng)組件集成得很好,而現(xiàn)在在 iOS 7 中又能實(shí)現(xiàn)自定義屏幕之間的轉(zhuǎn)換逐纬,功能實(shí)在是太強(qiáng)大了蛔屹。
有了 VIPER,視圖控制器便就能真正的做它本來(lái)應(yīng)該做的事情了豁生,那就是控制視圖兔毒。 我們的待辦事項(xiàng)應(yīng)擁有兩個(gè)視圖控制器,一個(gè)是列表視圖沛硅,另一個(gè)是新建待辦眼刃。因?yàn)?add view controller 要做的所有事情就是控制視圖,所以實(shí)現(xiàn)起來(lái)非常的簡(jiǎn)單基礎(chǔ):
1. @implementation VTDAddViewController
3. - (void)viewDidAppear:(BOOL)animated
4. {
5. [super viewDidAppear:animated];
7. UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
8. action:@selector(dismiss)];
9. [self.transitioningBackgroundView addGestureRecognizer:gestureRecognizer];
10. self.transitioningBackgroundView.userInteractionEnabled = YES;
11. }
13. - (void)dismiss
14. {
15. [self.eventHandler cancelAddAction];
16. }
18. - (void)setEntryName:(NSString *)name
19. {
20. self.nameTextField.text = name;
21. }
23. - (void)setEntryDueDate:(NSDate *)date
24. {
25. [self.datePicker setDate:date];
26. }
28. - (IBAction)save:(id)sender
29. {
30. [self.eventHandler saveAddActionWithName:self.nameTextField.text
31. dueDate:self.datePicker.date];
32. }
34. - (IBAction)cancel:(id)sender
35. {
36. [self.eventHandler cancelAddAction];
37. }
40. #pragma mark - UITextFieldDelegate Methods
42. - (BOOL)textFieldShouldReturn:(UITextField *)textField
43. {
44. [textField resignFirstResponder];
46. return YES;
47. }
49. @end
應(yīng)用在接入網(wǎng)絡(luò)以后會(huì)變得更有用處摇肌,但是究竟該在什么時(shí)候聯(lián)網(wǎng)呢擂红?又由誰(shuí)來(lái)負(fù)責(zé)啟動(dòng)網(wǎng)絡(luò)連接呢?典型的情況下围小,由交互器來(lái)啟動(dòng)網(wǎng)絡(luò)連接操作的項(xiàng)目昵骤,但是它不會(huì)直接處理網(wǎng)絡(luò)代碼。它會(huì)尋找一個(gè)像是 network manager 或者 API client 這樣的依賴項(xiàng)肯适。交互器可能聚合來(lái)自多個(gè)源的數(shù)據(jù)來(lái)提供所需的信息变秦,從而完成一個(gè)用例。最終框舔,就由展示器來(lái)采集交互器反饋的數(shù)據(jù)蹦玫,然后組織并進(jìn)行展示赎婚。
數(shù)據(jù)存儲(chǔ)模塊負(fù)責(zé)提供實(shí)體給交互器。因?yàn)榻换テ饕瓿蓸I(yè)務(wù)邏輯樱溉,因此它需要從數(shù)據(jù)存儲(chǔ)中獲取實(shí)體并操縱它們挣输,然后將更新后的實(shí)體再放回?cái)?shù)據(jù)存儲(chǔ)中。數(shù)據(jù)存儲(chǔ)管理實(shí)體的持久化福贞,而實(shí)體應(yīng)該對(duì)數(shù)據(jù)庫(kù)全然不知撩嚼,正因如此,實(shí)體并不知道如何對(duì)自己進(jìn)行持久化挖帘。
交互器同樣不需要知道如何將實(shí)體持久化完丽,有時(shí)交互器更希望使用一個(gè) data manager 來(lái)使其與數(shù)據(jù)存儲(chǔ)的交互變得容易。Data manager 可以處理更多的針對(duì)存儲(chǔ)的操作拇舀,比如創(chuàng)建獲取請(qǐng)求逻族,構(gòu)建查詢等等。這就使交互器能夠?qū)⒏嗟淖⒁饬Ψ旁趹?yīng)用邏輯上你稚,而不必再了解實(shí)體是如何被聚集或持久化的瓷耙。下面我們舉一個(gè)例子來(lái)說(shuō)明使用 data manager 有意義的,這個(gè)例子假設(shè)你在使用 Core Data刁赖。這是示例應(yīng)用程序的 data manager 的接口:
1. @interface VTDListDataManager : NSObject
3. @property (nonatomic, strong) VTDCoreDataStore *dataStore;
5. - (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;
7. @end
當(dāng)使用 TDD 來(lái)開(kāi)發(fā)一個(gè)交互器時(shí),是可以用一個(gè)測(cè)試用的模擬存儲(chǔ)來(lái)代替生產(chǎn)環(huán)境的數(shù)據(jù)存儲(chǔ)的长搀。避免與遠(yuǎn)程服務(wù)器通訊(網(wǎng)絡(luò)服務(wù))以及避免讀取磁盤(pán)(數(shù)據(jù)庫(kù))可以加快你測(cè)試的速度并加強(qiáng)其可重復(fù)性宇弛。
將數(shù)據(jù)存儲(chǔ)保持為一個(gè)界限清晰的特定層的原因之一是,這可以讓你延遲選擇一個(gè)特定的持久化技術(shù)源请。如果你的數(shù)據(jù)存儲(chǔ)是一個(gè)獨(dú)立的類枪芒,那你就可以使用一個(gè)基礎(chǔ)的持久化策略來(lái)開(kāi)始你的應(yīng)用,然后等到有意義的時(shí)候升級(jí)至 SQLite 或者 Core Data谁尸。而因?yàn)閿?shù)據(jù)存儲(chǔ)層的存在舅踪,你的應(yīng)用代碼庫(kù)中就不需要改變?nèi)魏螙|西。
在 iOS 的項(xiàng)目中使用 Core Data 經(jīng)常比構(gòu)架本身還容易引起更多爭(zhēng)議良蛮。然而抽碌,利用 VIPER 來(lái)使用 Core Data 將給你帶來(lái)使用 Core Data 的前所未有的良好體驗(yàn)。在持久化數(shù)據(jù)的工具層面上决瞳,Core Data 可以保持快速存取和低內(nèi)存占用方面货徙,簡(jiǎn)直是個(gè)神器。但是有個(gè)很惱人的地方皮胡,它會(huì)像觸須一樣把 NSManagedObjectContext 延伸至你所有的應(yīng)用實(shí)現(xiàn)文件中痴颊,特別是那些它們不該待的地方。VIPER 可以使 Core Data 待在正確的地方:數(shù)據(jù)存儲(chǔ)層屡贺。
在待辦事項(xiàng)示例中蠢棱,應(yīng)用僅有的兩部分知道使用了 Core Data锌杀,其一是數(shù)據(jù)存儲(chǔ)本身,它負(fù)責(zé)建立 Core Data 堆棧泻仙;另一個(gè)是 data manager抛丽。Data manager 執(zhí)行了獲取請(qǐng)求,將數(shù)據(jù)存儲(chǔ)返回的 NSManagedObject 對(duì)象轉(zhuǎn)換為標(biāo)準(zhǔn)的 PONSO 模型對(duì)象饰豺,并傳輸回業(yè)務(wù)邏輯層亿鲜。這樣一來(lái),應(yīng)用程序核心將不再依賴于 Core Data冤吨,附加得到的好處是蒿柳,你也再也不用擔(dān)心過(guò)期數(shù)據(jù) (stale) 和沒(méi)有良好組織的多線程 NSManagedObjects 來(lái)糟蹋你的工作成果了。
在通過(guò)請(qǐng)求訪問(wèn) Core Data 存儲(chǔ)時(shí)漩蟆,data manager 中看起來(lái)是這樣的:
1. @implementation VTDListDataManager
3. - (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate*)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock
4. {
5. NSCalendar *calendar = [NSCalendar autoupdatingCurrentCalendar];
7. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(date >= %@) AND (date <= %@)", [calendar dateForBeginningOfDay:startDate], [calendar dateForEndOfDay:endDate]];
8. NSArray *sortDescriptors = @[];
10. __weak typeof(self) welf = self;
11. [self.dataStore
12. fetchEntriesWithPredicate:predicate
13. sortDescriptors:sortDescriptors
14. completionBlock:^(NSArray* entries) {
15. if (completionBlock)
16. {
17. completionBlock([welf todoItemsFromDataStoreEntries:entries]);
18. }
19. }];
20. }
22. - (NSArray*)todoItemsFromDataStoreEntries:(NSArray *)entries
23. {
24. return [entries arrayFromObjectsCollectedWithBlock:^id(VTDManagedTodoItem *todo) {
25. return [VTDTodoItem todoItemWithDueDate:todo.date name:todo.name];
26. }];
27. }
29. @end
與 Core Data 一樣極富爭(zhēng)議的恐怕就是 UI 故事板了垒探。故事板具有很多有用的功能,如果完全忽視它將會(huì)是一個(gè)錯(cuò)誤怠李。然而圾叼,調(diào)用故事版所能提供的所有功能來(lái)完成 VIPER 的所有目標(biāo)仍然是很困難的。
我們所能做出的妥協(xié)就是選擇不使用 segues 捺癞。有時(shí)候使用 segues 是有效的夷蚊,但是使用 segues 的危險(xiǎn)性在于它們很難原封不動(dòng)地保持屏幕之間的分離,以及 UI 和應(yīng)用邏輯之間的分離髓介。一般來(lái)說(shuō)惕鼓,如果實(shí)現(xiàn) prepareForSegue 方法是必須的話,我們就盡量不去使用 segues唐础。
除此之外箱歧,故事板是一個(gè)實(shí)現(xiàn)用戶界面布局有效方法,特別是在使用自動(dòng)布局的時(shí)候一膨。我們選擇在實(shí)現(xiàn)待辦事項(xiàng)兩個(gè)界面的實(shí)例中使用故事板呀邢,并且使用這樣的代碼來(lái)執(zhí)行自己的導(dǎo)航操作。
1. static NSString *ListViewControllerIdentifier = @"VTDListViewController";
3. @implementation VTDListWireframe
5. - (void)presentListInterfaceFromWindow:(UIWindow *)window
6. {
7. VTDListViewController *listViewController = [self listViewControllerFromStoryboard];
8. listViewController.eventHandler = self.listPresenter;
9. self.listPresenter.userInterface = listViewController;
10. self.listViewController = listViewController;
12. [self.rootWireframe showRootViewController:listViewController
13. inWindow:window];
14. }
16. - (VTDListViewController *)listViewControllerFromStoryboard
17. {
18. UIStoryboard *storyboard = [self mainStoryboard];
19. VTDListViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:ListViewControllerIdentifier];
20. return viewController;
21. }
23. - (UIStoryboard *)mainStoryboard
24. {
25. UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main"
26. bundle:[NSBundle mainBundle]];
27. return storyboard;
28. }
30. @end
使用 VIPER 構(gòu)建模塊
一般在使用 VIPER 的時(shí)候豹绪,你會(huì)發(fā)現(xiàn)一個(gè)屏幕或一組屏幕傾向于聚在一起作為一個(gè)模塊价淌。模塊可以以多種形式體現(xiàn),但一般最好把它想成是一種特性森篷。在播客應(yīng)用中输钩,一個(gè)模塊可能是音頻播放器或訂閱瀏覽器。然而在我們的待辦事項(xiàng)應(yīng)用中仲智,列表和添加事項(xiàng)的屏幕都將作為單獨(dú)的模塊被建立买乃。
將你的應(yīng)用作為一組模塊來(lái)設(shè)計(jì)有很多好處,其中之一就是模塊可以有非常明確和定義良好的接口钓辆,并且獨(dú)立于其他的模塊剪验。這就使增加或者移除特性變得更加簡(jiǎn)單肴焊,也使在界面中向用戶展示各種可變模塊變得更加簡(jiǎn)單。
我們希望能將待辦事項(xiàng)中各模塊之間分隔更加明確功戚,我們?yōu)樘砑幽K定義了兩個(gè)協(xié)議娶眷。一個(gè)是模塊接口,它定義了模塊可以做什么啸臀;另一個(gè)則是模塊的代理届宠,用來(lái)描述該模塊做了什么。例如:
1. @protocol VTDAddModuleInterface
3. - (void)cancelAddAction;
4. - (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;
6. @end
9. @protocol VTDAddModuleDelegate
11. - (void)addModuleDidCancelAddAction;
12. - (void)addModuleDidSaveAddAction;
14. @end
因?yàn)槟K必須要被展示乘粒,才能對(duì)用戶產(chǎn)生價(jià)值豌注,所以模塊的展示器通常需要實(shí)現(xiàn)模型的接口。當(dāng)另一個(gè)模型想要展現(xiàn)當(dāng)前模塊時(shí)灯萍,它的展示器就需要實(shí)現(xiàn)模型的委托協(xié)議轧铁,這樣它就能在展示時(shí)知道當(dāng)前模塊做了些什么。
一個(gè)模塊可能包括實(shí)體旦棉,交互器和管理器的通用應(yīng)用邏輯層齿风,這些通常可用于多個(gè)屏幕绑洛。當(dāng)然救斑,這取決于這些屏幕之間的交互及它們的相似度。一個(gè)模塊可以像在待辦事項(xiàng)列表里面一樣诊笤,簡(jiǎn)單的只代表一個(gè)屏幕系谐。這樣一來(lái),應(yīng)用邏輯層對(duì)于它的特定模塊的行為來(lái)說(shuō)就非常特有了讨跟。
模塊同樣是組織代碼的簡(jiǎn)便途徑。將模塊所有的編碼都放在它自己的文件夾中并在 Xcode 中建一個(gè) group鄙煤,這會(huì)在你需要尋找和改變更加容易晾匠。當(dāng)你在要尋找一個(gè)類時(shí),它恰到好處地就在你所期待的地方梯刚,這種感覺(jué)真是無(wú)法形容的棒凉馆。
利用 VIPER 建立模塊的另一個(gè)好處是它使得擴(kuò)展到多平臺(tái)時(shí)變得更加簡(jiǎn)單。獨(dú)立在交互器層中的所有用例的應(yīng)用邏輯允許你可以專注于為平板亡资,電話或者 Mac 構(gòu)建新的用戶界面澜共,同時(shí)可以重用你的應(yīng)用層。
進(jìn)一步來(lái)說(shuō)锥腻,iPad 應(yīng)用的用戶界面能夠?qū)⒉糠?iPhone 應(yīng)用的視圖嗦董,視圖控制器及展示器進(jìn)行再利用。在這種情況下瘦黑,iPad 屏幕將由 ‘super’ 展示器和線框來(lái)代表京革,這樣可以利用 iPhone 使用過(guò)的展示器和線框來(lái)組成屏幕奇唤。建立進(jìn)而維護(hù)一個(gè)跨多平臺(tái)的應(yīng)用是一個(gè)巨大的挑戰(zhàn),但是好的構(gòu)架可以對(duì)整個(gè)模型和應(yīng)用層的再利用有大幅度的提升匹摇,并使其實(shí)現(xiàn)起來(lái)更加容易咬扇。
利用 VIPER 進(jìn)行測(cè)試
VIPER 的出現(xiàn)激發(fā)了一個(gè)關(guān)注點(diǎn)的分離,這使得采用 TDD 變得更加簡(jiǎn)便廊勃。交互器包含獨(dú)立與任何 UI 的純粹邏輯懈贺,這使測(cè)試驅(qū)動(dòng)開(kāi)發(fā)更加簡(jiǎn)單。同時(shí)展示器包含用來(lái)為顯示準(zhǔn)備數(shù)據(jù)的邏輯坡垫,并且它也獨(dú)立于任何一個(gè) UIKit 部件梭灿。對(duì)于這個(gè)邏輯的開(kāi)發(fā)也很容易用測(cè)試來(lái)驅(qū)動(dòng)。
我們更傾向于先從交互器下手葛虐。用戶界面里所有部分都服務(wù)于用例胎源,而通過(guò)采用 TDD 來(lái)測(cè)試驅(qū)動(dòng)交互器的 API 可以讓你對(duì)用戶界面和用例之間的關(guān)系有一個(gè)更好的了解。
作為實(shí)例屿脐,我們來(lái)看一下負(fù)責(zé)待辦事項(xiàng)列表的交互器涕蚤。尋找待辦事項(xiàng)的策略是要找出所有的將在下周末前截止的項(xiàng)目,并將這些項(xiàng)目分別歸類至截止于今天的诵,明天万栅,本周或者下周。
我們編寫(xiě)的第一個(gè)測(cè)試是為了保證交互器能夠找到所有的截止于下周末的待辦事項(xiàng):
1. - (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek
2. {
3. [[self.dataManager expect] todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY];
4. [self.interactor findUpcomingItems];
5. }
一旦知道了交互器找到了正確的待辦事項(xiàng)后西疤,我們就需要編寫(xiě)幾個(gè)小測(cè)試用來(lái)確認(rèn)它確實(shí)將待辦事項(xiàng)分配到了正確的相對(duì)日期組內(nèi)(比如說(shuō)今天烦粒,明天,等等)代赁。
1. - (void)testFindingUpcomingItemsWithOneItemDueTodayReturnsOneUpcomingItemsForToday
2. {
3. NSArray *todoItems = @[[VTDTodoItem todoItemWithDueDate:self.today name:@"Item 1"]];
4. [self dataStoreWillReturnToDoItems:todoItems];
6. NSArray *upcomingItems = @[[VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationToday dueDate:self.today title:@"Item 1"]];
7. [self expectUpcomingItems:upcomingItems];
9. [self.interactor findUpcomingItems];
10. }
既然我們已經(jīng)知道了交互器的 API 長(zhǎng)什么樣扰她,接下來(lái)就是開(kāi)發(fā)展示器。一旦展示器接收到了交互器傳來(lái)的待辦事項(xiàng)芭碍,我們就需要測(cè)試看看我們是否適當(dāng)?shù)膶?shù)據(jù)進(jìn)行格式化并且在用戶界面中正確的顯示它徒役。
1. - (void)testFoundZeroUpcomingItemsDisplaysNoContentMessage
2. {
3. [[self.ui expect] showNoContentMessage];
5. [self.presenter foundUpcomingItems:@[]];
6. }
8. - (void)testFoundUpcomingItemForTodayDisplaysUpcomingDataWithNoDay
9. {
10. VTDUpcomingDisplayData *displayData = [self displayDataWithSectionName:@"Today"
11. sectionImageName:@"check"
12. itemTitle:@"Get a haircut"
13. itemDueDay:@""];
14. [[self.ui expect] showUpcomingDisplayData:displayData];
16. NSCalendar *calendar = [NSCalendar gregorianCalendar];
17. NSDate *dueDate = [calendar dateWithYear:2014 month:5 day:29];
18. VTDUpcomingItem *haircut = [VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationToday dueDate:dueDate title:@"Get a haircut"];
20. [self.presenter foundUpcomingItems:@[haircut]];
21. }
23. - (void)testFoundUpcomingItemForTomorrowDisplaysUpcomingDataWithDay
24. {
25. VTDUpcomingDisplayData *displayData = [self displayDataWithSectionName:@"Tomorrow"
26. sectionImageName:@"alarm"
27. itemTitle:@"Buy groceries"
28. itemDueDay:@"Thursday"];
29. [[self.ui expect] showUpcomingDisplayData:displayData];
31. NSCalendar *calendar = [NSCalendar gregorianCalendar];
32. NSDate *dueDate = [calendar dateWithYear:2014 month:5 day:29];
33. VTDUpcomingItem *groceries = [VTDUpcomingItem upcomingItemWithDateRelation:VTDNearTermDateRelationTomorrow dueDate:dueDate title:@"Buy groceries"];
35. [self.presenter foundUpcomingItems:@[groceries]];
36. }
同樣需要測(cè)試的是應(yīng)用是否在用戶想要新建待辦事項(xiàng)時(shí)正確啟動(dòng)了相應(yīng)操作:
1. - (void)testAddNewToDoItemActionPresentsAddToDoUI
2. {
3. [[self.wireframe expect] presentAddInterface];
5. [self.presenter addNewEntry];
6. }
這時(shí)我們可以開(kāi)發(fā)視圖功能了,并且在沒(méi)有待辦事項(xiàng)的時(shí)候我們想要展示一個(gè)特殊的信息窖壕。
1. - (void)testShowingNoContentMessageShowsNoContentView
2. {
3. [self.view showNoContentMessage];
5. XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");
6. }
有待辦事項(xiàng)出現(xiàn)時(shí)忧勿,我們要確保列表是顯示出來(lái)的:
1. - (void)testShowingUpcomingItemsShowsTableView
2. {
3. [self.view showUpcomingDisplayData:nil];
5. XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");
6. }
首先建立交互器是一種符合 TDD 的自然規(guī)律。如果你首先開(kāi)發(fā)交互器瞻讽,緊接著是展示器鸳吸,你就可以首先建立一個(gè)位于這些層的套件測(cè)試,并且為實(shí)現(xiàn)這是實(shí)例奠定基礎(chǔ)速勇。由于你不需要為了測(cè)試它們而去與用戶界面進(jìn)行交互晌砾,所以這些類可以進(jìn)行快速迭代。在你需要開(kāi)發(fā)視圖的時(shí)候快集,你會(huì)有一個(gè)可以工作并測(cè)試過(guò)的邏輯和表現(xiàn)層來(lái)與其進(jìn)行連接贡羔。在快要完成對(duì)視圖的開(kāi)發(fā)時(shí)廉白,你會(huì)發(fā)現(xiàn)第一次運(yùn)行程序時(shí)所有部件都運(yùn)行良好,因?yàn)槟闼幸淹ㄟ^(guò)的測(cè)試已經(jīng)告訴你它可以工作乖寒。
結(jié)論
我們希望你喜歡這篇對(duì) VIPER 的介紹猴蹂。或許你們都很好奇接下來(lái)應(yīng)該做什么楣嘁,如果你希望通過(guò) VIPER 來(lái)對(duì)你下一個(gè)應(yīng)用進(jìn)行設(shè)計(jì)磅轻,該從哪里開(kāi)始呢?
我們竭盡全力使這篇文章和我們利用 VIPER 實(shí)現(xiàn)的應(yīng)用實(shí)例足夠明確并且進(jìn)行了很好的定義逐虚。我們的待辦事項(xiàng)里列表程序相當(dāng)直接簡(jiǎn)單聋溜,但是它準(zhǔn)確地解釋了如何利用 VIPER 來(lái)建立一個(gè)應(yīng)用。在實(shí)際的項(xiàng)目中叭爱,你可以根據(jù)你自己的挑戰(zhàn)和約束條件來(lái)決定要如何實(shí)踐這個(gè)例子撮躁。根據(jù)以往的經(jīng)驗(yàn),我們的每個(gè)項(xiàng)目在使用 VIPER 時(shí)都或多或少地改變了一些策略买雾,但它們無(wú)一例外的都從中得益把曼,找到了正確的方向。
很多情況下由于某些原因漓穿,你可能會(huì)想要偏離 VIPER 所指引的道路嗤军。可能你遇到了很多 'bunny' 對(duì)象晃危,或者你的應(yīng)用使用了故事板的 segues叙赚。沒(méi)關(guān)系的,在這些情況下僚饭,你只需要在做決定時(shí)稍微考慮下 VIPER 所代表的精神就好震叮。VIPER 的核心在于它是建立在單一責(zé)任原則上的架構(gòu)。如果你碰到了些許麻煩鳍鸵,想想這些原則再考慮如何前進(jìn)冤荆。
你一定想知道在現(xiàn)有的應(yīng)用中能否只用 VIPER 。在這種情況下权纤,你可以考慮使用 VIPER 構(gòu)建新的特性。我們?cè)S多現(xiàn)有項(xiàng)目都使用了這個(gè)方法乌妒。你可以利用 VIPER 建立一個(gè)模塊汹想,這能幫助你發(fā)現(xiàn)許多建立在單一責(zé)任原則基礎(chǔ)上造成難以運(yùn)用架構(gòu)的現(xiàn)有問(wèn)題。
軟件開(kāi)發(fā)最偉大的事情之一就是每個(gè)應(yīng)用程序都是不同的撤蚊,而設(shè)計(jì)每個(gè)應(yīng)用的架構(gòu)的方式也是不同的古掏。這就意味著每個(gè)應(yīng)用對(duì)于我們來(lái)說(shuō)都是一個(gè)學(xué)習(xí)和嘗試的機(jī)遇,如果你決定開(kāi)始使用 VIPER侦啸,你會(huì)受益匪淺槽唾。感謝你的閱讀丧枪。
Swift 補(bǔ)充
蘋(píng)果上周在 WWDC 介紹了一門(mén)稱之為 Swift 的編程語(yǔ)言來(lái)作為 Cocoa 和 Cocoa Touch 開(kāi)發(fā)的未來(lái)。現(xiàn)在發(fā)表關(guān)于 Swift 的完整意見(jiàn)還為時(shí)尚早庞萍,但眾所周知編程語(yǔ)言對(duì)我們?nèi)绾卧O(shè)計(jì)和構(gòu)建應(yīng)用有著重大影響拧烦。我們決定使用 Swift 重寫(xiě)我們的待辦事項(xiàng)清單,幫助我們學(xué)習(xí)它對(duì) VIPER 意味著什么钝计。至今為止恋博,收獲頗豐。Swift 中的一些特性對(duì)于構(gòu)建應(yīng)用的體驗(yàn)有著顯著的提升私恬。
結(jié)構(gòu)體
在 VIPER 中我們使用小型债沮,輕量級(jí)的 model 類來(lái)在比如從展示器到視圖這樣不同的層間傳遞數(shù)據(jù)。這些 PONSOs 通常是只是簡(jiǎn)單地帶有少量數(shù)據(jù)本鸣,并且通常這些類不會(huì)被繼承疫衩。Swift 的結(jié)構(gòu)體非常適合這個(gè)情況。下面的結(jié)構(gòu)體的例子來(lái)自 VIPER Swift荣德。這個(gè)結(jié)構(gòu)體需要被判斷是否相等闷煤,所以我們重載了 == 操作符來(lái)比較這個(gè)類型的兩個(gè)實(shí)例。
1. struct UpcomingDisplayItem : Equatable, Printable {
2. let title : String = ""
3. let dueDate : String = ""
5. var description : String { get {
6. return "\(title) -- \(dueDate)"
7. }}
9. init(title: String, dueDate: String) {
10. self.title = title
11. self.dueDate = dueDate
12. }
13. }
15. func == (leftSide: UpcomingDisplayItem, rightSide: UpcomingDisplayItem) -> Bool {
16. var hasEqualSections = false
17. hasEqualSections = rightSide.title == leftSide.title
19. if hasEqualSections == false {
20. return false
21. }
23. hasEqualSections = rightSide.dueDate == rightSide.dueDate
25. return hasEqualSections
26. }
類型安全
也許 Objective-C 和 Swift 的最大區(qū)別是它們?cè)趯?duì)于類型處理上的不同命爬。 Objective-C 是動(dòng)態(tài)類型曹傀,而 Swift 故意在編譯時(shí)做了嚴(yán)格的類型檢查。對(duì)于一個(gè)類似 VIPER 的架構(gòu)饲宛, 應(yīng)用由不同層構(gòu)成皆愉,類型安全是提升程序員效率和設(shè)計(jì)架構(gòu)有非常大的好處。編譯器幫助你確保正確類型的容器和對(duì)象在層的邊界傳遞艇抠。如上所示幕庐,這是一個(gè)使用結(jié)構(gòu)體的好地方。如果一個(gè)結(jié)構(gòu)體的被設(shè)計(jì)為存在于兩層之間家淤,那么由于類型安全异剥,你可以保證它將永遠(yuǎn)無(wú)法脫離這些層之間。
擴(kuò)展閱讀