使用VIPER構(gòu)建iOS應(yīng)用

使用VIPER構(gòu)建iOS應(yīng)用

2014-07-03 09:53 編輯: qiancheng 分類:iOS開(kāi)發(fā) 來(lái)源:CocoaChina

iOS應(yīng)用VIPER

轉(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è)試各層間的邊界處的交互:

8370_140703100012_1.jpg

大部分 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)的:

8370_140703100110_1.png

雖然在應(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ò)展閱讀

VIPER TODO, 文章示例

VIPER SWIFT, 基于 Swift 的文章示例

另一個(gè)計(jì)數(shù)器應(yīng)用

Mutual Mobile 關(guān)于 VIPER 的介紹

簡(jiǎn)明架構(gòu)

更輕量的 View Controllers

測(cè)試 View Controllers

Bunnies

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末絮重,一起剝皮案震驚了整個(gè)濱河市冤寿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌青伤,老刑警劉巖督怜,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異狠角,居然都是意外死亡号杠,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)姨蟋,“玉大人屉凯,你說(shuō)我怎么就攤上這事⊙廴埽” “怎么了悠砚?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)偷仿。 經(jīng)常有香客問(wèn)我哩簿,道長(zhǎng),這世上最難降的妖魔是什么酝静? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任节榜,我火速辦了婚禮,結(jié)果婚禮上别智,老公的妹妹穿的比我還像新娘宗苍。我一直安慰自己,他們只是感情好薄榛,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布讳窟。 她就那樣靜靜地躺著,像睡著了一般敞恋。 火紅的嫁衣襯著肌膚如雪丽啡。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,328評(píng)論 1 310
  • 那天硬猫,我揣著相機(jī)與錄音补箍,去河邊找鬼。 笑死啸蜜,一個(gè)胖子當(dāng)著我的面吹牛坑雅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播衬横,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼裹粤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了蜂林?” 一聲冷哼從身側(cè)響起遥诉,我...
    開(kāi)封第一講書(shū)人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎噪叙,沒(méi)想到半個(gè)月后突那,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡构眯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了早龟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惫霸。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡猫缭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出壹店,到底是詐尸還是另有隱情猜丹,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布硅卢,位于F島的核電站射窒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏将塑。R本人自食惡果不足惜脉顿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望点寥。 院中可真熱鬧艾疟,春花似錦、人聲如沸敢辩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)戚长。三九已至盗冷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間同廉,已是汗流浹背仪糖。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留恤溶,地道東北人乓诽。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像咒程,于是被迫代替她去往敵國(guó)和親鸠天。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容

  • 建筑領(lǐng)域流行這樣一句話帐姻,“我們雖然在營(yíng)造建筑稠集,但建筑也會(huì)重新塑造我們”。正如所有開(kāi)發(fā)者最終領(lǐng)悟到的饥瓷,這句話同樣適用...
    Answer55閱讀 749評(píng)論 0 3
  • 【編者按】本篇文章由 Jeff Gilbert 和 Conrad Stoll 共同編寫(xiě)剥纷,通過(guò)構(gòu)建一個(gè)基礎(chǔ)示例應(yīng)用,...
    OneAPM閱讀 856評(píng)論 0 8
  • 原文鏈接: Architecting iOS Apps with VIPER 哥們認(rèn)為呢铆,這誒老外很好的詮釋了iOS...
    HunterDude閱讀 2,023評(píng)論 2 8
  • 第一次玩沙畫(huà)晦鞋,可興奮了。 我要做一些草給牛吃。 我要做個(gè)小腳丫 我要開(kāi)始建小牛的家啦悠垛。 最后變成了堆雪人 能猜出來(lái)...
    夢(mèng)鐵凝閱讀 387評(píng)論 0 2
  • 姓名:富智燚 361期努力一組 單位:深圳市蔚藍(lán)時(shí)代商業(yè)管理有限公司海南分公司 【日精進(jìn)打卡第31天】 【知~學(xué)習(xí)...
    復(fù)制1閱讀 126評(píng)論 0 0