哥們認(rèn)為晓铆,這誒老外很好的詮釋了iOS的VIPER架構(gòu),所以將此文翻譯過來笙蒙,大家觀看咨跌,希望大家喜歡。在下一篇文章中我會將公司的VIPER架構(gòu)分享給大家辫继。
居然又做了件白癡的事兒阁最,翻譯完才曉得。之前已經(jīng)有人已經(jīng)翻譯過了骇两。不過沒關(guān)系,學(xué)習(xí)嗎姜盈。你說我抄襲低千? NO. 哥們英文雖然不好但是有這個。
關(guān)于架構(gòu)馏颂,這篇文章寫的不錯示血,有興趣的同學(xué)可以看看iOS 架構(gòu)模式 - 簡述 MVC, MVP, MVVM 和 VIPER (譯)
文章結(jié)構(gòu):
What is VIPER?
APPlication Design Based on Use Cases
Main Part of VIPER
- View
- Interactor
- Presenter
- Entity
- Routing
APPlication Components Fitting in with VIPER
Using VIPER to Build Modules
Testing with VIPER
Conclusion
Swift Addendum
Structs
Type Safety
在建筑領(lǐng)域,我們塑造了我們的建筑救拉,眾所周知难审,后來我們的建筑塑造了我們。 隨著所有程序員最終學(xué)習(xí)亿絮,這也適用于構(gòu)建軟件告喊。
重要的是設(shè)計我們的代碼,以便每個片段都是容易識別派昧,具有明確的目的黔姜,并與其他部分以邏輯方式相配合。 這就是我們所說的軟件架構(gòu)蒂萎。 良好的架構(gòu)不是什么使產(chǎn)品成功秆吵,但它確實使產(chǎn)品可維護(hù),并幫助保持人們的理智保持它五慈!
在本文中纳寂,我們將介紹一種稱為VIPER的iOS應(yīng)用程序架構(gòu)方法。 VIPER已被用于構(gòu)建許多大型項目泻拦,但為了本文的目的毙芜,我們將通過構(gòu)建一個待辦事項列表應(yīng)用程序向您展示VIPER。 你可以按照GitHub上的示例項目:
What is VIPER?
測試并不總是構(gòu)建iOS應(yīng)用程序的主要部分聪轿。 當(dāng)我們開始尋求改進(jìn)Mutual Mobile的測試實踐時爷肝,我們發(fā)現(xiàn)為iOS應(yīng)用程序編寫測試很困難。 我們決定,如果我們要改進(jìn)我們測試軟件的方式灯抛,我們首先需要提出一個更好的方式來構(gòu)建我們的應(yīng)用程序金赦。 我們稱之為VIPER。
VIPER是一個應(yīng)用程序的清潔架構(gòu)的iOS應(yīng)用程序对嚼。 VIPER這個詞是View夹抗,Interactor,Presenter纵竖,Entity和Routing漠烧。 清潔架構(gòu)將應(yīng)用程序的邏輯結(jié)構(gòu)劃分為不同的責(zé)任層。 這使得更容易隔離依賴(例如您的數(shù)據(jù)庫)和測試在層之間的邊界的交互:
大多數(shù)iOS應(yīng)用程序使用MVC(模型 - 視圖控制器)架構(gòu)靡砌。 使用MVC作為應(yīng)用程序架構(gòu)可以引導(dǎo)您思考每個類是模型已脓,視圖或控制器。 由于許多應(yīng)用程序邏輯不屬于模型或視圖通殃,因此通常最終在控制器中度液。 這導(dǎo)致稱為臃腫視圖控制器的問題,其中視圖控制器最終做太多画舌。 減少這些大規(guī)模的視圖控制器并不是iOS開發(fā)者尋求提高代碼質(zhì)量的唯一挑戰(zhàn)堕担,但它是一個很好的開始。
VIPER的獨(dú)特層通過為應(yīng)用邏輯和導(dǎo)航相關(guān)代碼提供清晰的位置來幫助應(yīng)對這一挑戰(zhàn)曲聂。 通過應(yīng)用VIPER霹购,您將注意到我們的待辦事項列表示例中的視圖控制器是精簡,平均朋腋,視圖控制機(jī)器齐疙。 您還會發(fā)現(xiàn)視圖控制器和所有其他類中的代碼很容易理解,更容易測試旭咽,因此也更容易維護(hù)剂碴。
Application Design Based on Use Cases
應(yīng)用程序通常作為一組用例來實現(xiàn)。 用例也稱為接受標(biāo)準(zhǔn)或行為轻专,并描述應(yīng)用程序意圖做什么忆矛。 也許列表需要按日期,類型或名稱排序请垛。 這是一個用例催训。 用例是負(fù)責(zé)業(yè)務(wù)邏輯的應(yīng)用程序?qū)印?用例應(yīng)該獨(dú)立于它們的用戶界面實現(xiàn)。 它們也應(yīng)當(dāng)小而明確宗收。 決定如何將復(fù)雜的應(yīng)用程序分解成更小的用例是具有挑戰(zhàn)性的漫拭,需要練習(xí),但它是一個有用的方式來限制每個問題的范圍混稽,你正在寫的每個類采驻。
使用VIPER構(gòu)建應(yīng)用程序涉及實現(xiàn)一組組件以滿足每個用例审胚。 應(yīng)用程序邏輯是實現(xiàn)用例的主要部分,但它不是唯一的部分礼旅。 用例也會影響用戶界面膳叨。 此外,考慮如何將應(yīng)用程序與應(yīng)用程序的其他核心組件(如網(wǎng)絡(luò)和數(shù)據(jù)持久性)相結(jié)合很重要痘系。 組件像插件一樣用于用例菲嘴,VIPER是描述每個組件的作用以及它們?nèi)绾蜗嗷ソ换サ囊环N方式。
我們的待辦事項列表應(yīng)用程序的用例或要求之一是基于用戶的選擇以不同的方式將待辦事項分組汰翠。 通過將組織數(shù)據(jù)的邏輯分離為用例龄坪,我們能夠保持用戶界面代碼干凈,并且可以輕松地在測試中包裝用例复唤,以確保它繼續(xù)按照我們的預(yù)期方式工作健田。
Main Parts of VIPER
The main parts of VIPER are:
- View: displays what it is told to by the Presenter and relays user input back to the Presenter.
(視圖:顯示Presenter告知的內(nèi)容,并將用戶輸入中繼回Presenter佛纫。)
- Interactor: contains the business logic as specified by a use case.
(交互器:包含用例指定的業(yè)務(wù)邏輯抄课。)
- Presenter: contains view logic for preparing content for display (as received from the Interactor) and for reacting to user inputs (by requesting new data from the Interactor).
(表示層,也可稱主持人:包含用于準(zhǔn)備顯示內(nèi)容(如從Interactor接收的)和用于對用戶輸入做出反應(yīng)(通過從Interactor請求新數(shù)據(jù))的視圖邏輯雳旅。)
- Entity: contains basic model objects used by the Interactor.
(實體:包含Interactor使用的基本模型對象。)
- Routing: contains navigation logic for describing which screens are shown in which order.
(路由:包含用于描述按哪個順序顯示哪些屏幕的導(dǎo)航邏輯间聊。)
這種分離也符合單一責(zé)任原則攒盈。 Interactor負(fù)責(zé)業(yè)務(wù)分析師,Presenter代表交互設(shè)計師哎榴,而View負(fù)責(zé)視覺設(shè)計師型豁。
下面是不同組件及其連接方式的圖表:
雖然VIPER的組件可以按任何順序在應(yīng)用程序中實現(xiàn),但我們選擇按照我們建議實施它們的順序介紹組件尚蝌。
您會注意到迎变,此順序與構(gòu)建整個應(yīng)用程序的過程大致一致,首先討論產(chǎn)品需要做什么飘言,然后討論用戶如何與之交互衣形。
Interactor (交互器)
交互器表示應(yīng)用程序中的單個用例。 它包含了操作模型對象(Entities)來執(zhí)行特定任務(wù)的業(yè)務(wù)邏輯姿鸿。 在Interactor中完成的工作應(yīng)該獨(dú)立于任何UI谆吴。 相同的Interactor可以在iOS應(yīng)用程序或OS X應(yīng)用程序中使用。
因為交互器是主要包含邏輯的PONSO(Plain Old NSObject)苛预,所以使用TDD很容易開發(fā)句狼。
示例應(yīng)用的主要用例是向用戶顯示任何即將到期的待完成項目(即下周末到期的任何項目)。 此用例的業(yè)務(wù)邏輯是找到在今天和下周末之間到期的任何待完成項目热某,并分配相對到期日:今天腻菇,明天胳螟,本周晚些或下周。
下面是從VTDListInteractor對應(yīng)的方法:
- (void)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 (實體)
實體是由交互器操作的模型對象筹吐。 實體僅由交互器操縱糖耸。 交互器從不將實體傳遞到表示層(即Presenter)。
實體也傾向于PONSO骏令。 如果使用Core Data蔬捷,您將希望受管對象保留在數(shù)據(jù)層之后。 Interactors不應(yīng)該與NSManagedObjects一起使用榔袋。
這里是我們的待辦事項的實體:
@interface VTDTodoItem : NSObject
@property (nonatomic, strong) NSDate* dueDate;
@property (nonatomic, copy) NSString* name;
+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;
@end
不要驚訝周拐,如果你的實體只是數(shù)據(jù)結(jié)構(gòu)。 任何與應(yīng)用程序相關(guān)的邏輯很可能在交互器中凰兑。
Presenter 主持人
Presenter是一個PONSO妥粟,主要由驅(qū)動UI的邏輯組成。 它知道何時呈現(xiàn)用戶界面吏够。 它從用戶交互收集輸入勾给,以便它可以更新UI并將請求發(fā)送到Interactor。
當(dāng)用戶點擊+按鈕添加一個新的待辦事項時锅知,addNewEntry被調(diào)用播急。 對于此操作,Presenter要求線框(wireframe)呈現(xiàn)用于添加新項目的UI:
- (void)addNewEntry
{
[self.listWireframe presentAddInterface];
}
Presenter還從Interactor接收結(jié)果售睹,并將結(jié)果轉(zhuǎn)換為在View中有效顯示的窗體桩警。
下面是從Interactor接收即將到來的項目的方法。 它將處理數(shù)據(jù)并確定向用戶顯示什么:
- (void)foundUpcomingItems:(NSArray*)upcomingItems
{
if ([upcomingItems count] == 0)
{
[self.userInterface showNoContentMessage];
}
else
{
[self updateUserInterfaceWithUpcomingItems:upcomingItems];
}
}
Entities從不從Interactor傳遞給Presenter昌妹。 相反捶枢,沒有行為的簡單數(shù)據(jù)結(jié)構(gòu)從Interactor傳遞給Presenter。 這防止在Presenter中完成任何“真正的工作”飞崖。 Presenter只能準(zhǔn)備要在View中顯示的數(shù)據(jù)烂叔。
View (View / ViewController)
視圖是被動的。 它等待Presenter給它顯示內(nèi)容; 它從不要求Presenter獲取數(shù)據(jù)固歪。 為View定義的方法(例如登錄屏幕的LoginView)應(yīng)允許Presenter以更高的抽象級別進(jìn)行通信蒜鸡,以其內(nèi)容表示,而不是如何顯示該內(nèi)容牢裳。 Presenter不知道UILabel术瓮,UIButton等的存在。Presenter只知道它維護(hù)的內(nèi)容和什么時候應(yīng)該顯示贰健。 由視圖決定內(nèi)容的顯示方式胞四。
View是一個抽象的接口,在Objective-C中用協(xié)議定義伶椿。 UIViewController或其子類之一將實現(xiàn)View協(xié)議辜伟。 例如氓侧,我們的示例中的“添加”屏幕具有以下界面:
@protocol VTDAddViewInterface <NSObject>
- (void)setEntryName:(NSString *)name;
- (void)setEntryDueDate:(NSDate *)date;
@end
當(dāng)用戶點擊取消按鈕時,視圖控制器告訴此事件處理程序用戶已經(jīng)指示它應(yīng)該取消添加操作导狡。 這樣约巷,事件處理程序可以處理解除添加視圖控制器并告知列表視圖進(jìn)行更新。
View和Presenter之間的邊界也是ReactiveCocoa的一個好地方旱捧。 在該示例中独郎,視圖控制器還可以提供用于返回表示按鈕動作的信號的方法。 這將允許演示者容易地響應(yīng)這些信號而不破壞責(zé)任的分離枚赡。
Routing (路由)
從一個屏幕到另一個屏幕的路由在交互設(shè)計器創(chuàng)建的wireframes中定義氓癌。 在VIPER中,路由的職責(zé)由兩個對象共享:Presenter和Wireframes贫橙。 wireframes對象擁有UIWindow贪婉,UINavigationController,UIViewController等卢肃。它負(fù)責(zé)創(chuàng)建一個View / ViewController并在窗口中安裝它疲迂。
由于Presenter包含對用戶輸入做出反應(yīng)的邏輯,因此Presenter知道何時導(dǎo)航到另一個屏幕莫湘,以及導(dǎo)航到哪個屏幕尤蒿。 同時,wireframe知道如何導(dǎo)航幅垮。 因此腰池,Presenter將使用wireframe來執(zhí)行導(dǎo)航。 它們一起描述從一個屏幕到下一個屏幕的路線军洼。
wireframe也是處理導(dǎo)航轉(zhuǎn)換動畫的一個明顯的地方。 看看這個例子從添加wireframe:
@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;
}
#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];
}
@end
應(yīng)用程序使用自定義視圖控制器轉(zhuǎn)換來呈現(xiàn)添加視圖控制器演怎。 由于wireframe負(fù)責(zé)執(zhí)行轉(zhuǎn)換匕争,它成為添加視圖控制器的轉(zhuǎn)換委托,并且可以返回適當(dāng)?shù)霓D(zhuǎn)換動畫爷耀。
Application Components Fitting in with VIPER (應(yīng)用組件與VIPER配合)
iOS應(yīng)用程序架構(gòu)需要考慮的事實是甘桑,UIKit和Cocoa Touch是應(yīng)用程序構(gòu)建的主要工具。 架構(gòu)需要與應(yīng)用程序的所有組件和平共存歹叮,但它還需要為框架的某些部分如何使用以及它們居住在哪里提供指導(dǎo)跑杭。
iOS應(yīng)用程序的主力是UIViewController
。 很容易假設(shè)替換MVC的競爭者將避免大量使用視圖控制器咆耿。 但視圖控制器是平臺的核心:它們處理方向變化德谅,響應(yīng)用戶的輸入,與系統(tǒng)組件(如導(dǎo)航控制器)集成萨螺,現(xiàn)在使用iOS 7窄做,允許在屏幕之間進(jìn)行自定義轉(zhuǎn)換愧驱。 它們非常有用。
有了VIPER椭盏,一個視圖控制器完全正是它的意思:它控制視圖组砚。 我們的待辦事項列表應(yīng)用程序有兩個視圖控制器,一個用于列表屏幕掏颊,一個用于添加屏幕糟红。 添加視圖控制器實現(xiàn)是非常基本的乌叶,因為它所要做的就是控制視圖:
@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
當(dāng)應(yīng)用程序連接到網(wǎng)絡(luò)時盆偿,它們通常更具吸引力。 但是枉昏,這個網(wǎng)絡(luò)應(yīng)該在哪里發(fā)生陈肛,什么應(yīng)該負(fù)責(zé)啟動它? 通常由Interactor來啟動網(wǎng)絡(luò)操作兄裂,但它不會直接處理網(wǎng)絡(luò)代碼句旱。 它會詢問依賴關(guān)系,如網(wǎng)絡(luò)管理器或API客戶端晰奖。 交互器可能必須聚合來自多個源的數(shù)據(jù)以提供滿足用例所需的信息谈撒。 然后由Presenter接收由Interactor返回的數(shù)據(jù)并將其格式化以用于呈現(xiàn)。
數(shù)據(jù)存儲器負(fù)責(zé)向交互器提供實體匾南。 當(dāng)交互器應(yīng)用其業(yè)務(wù)邏輯時啃匿,它將需要從數(shù)據(jù)存儲中檢索實體,操縱實體蛆楞,然后將更新的實體放回數(shù)據(jù)存儲中溯乒。 數(shù)據(jù)存儲管理實體的持久性。 實體不知道數(shù)據(jù)存儲豹爹,所以實體不知道如何堅持自己裆悄。
Interactor不應(yīng)該知道如何持久化實體。 有時Interactor可能想使用一種稱為數(shù)據(jù)管理器的對象來促進(jìn)它與數(shù)據(jù)存儲的交互臂聋。 數(shù)據(jù)管理器處理更多的存儲特定類型的操作光稼,例如創(chuàng)建獲取請求,構(gòu)建查詢等孩等。這允許交互器更多地關(guān)注應(yīng)用邏輯艾君,并且不必知道如何收集或持久化實體。 使用數(shù)據(jù)管理器有意義的一個例子是當(dāng)您使用Core Data時肄方,如下所述冰垄。
Here’s the interface for the example app’s data manager:
以下是范例應(yīng)用程式資料管理員的介面:
@interface VTDListDataManager : NSObject
@property (nonatomic, strong) VTDCoreDataStore *dataStore;
- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;
@end
當(dāng)使用TDD開發(fā)交互器時,可以使用測試雙/模擬切換生產(chǎn)數(shù)據(jù)存儲权她。 不與遠(yuǎn)程服務(wù)器通信(對于Web服務(wù))或觸摸磁盤(對于數(shù)據(jù)庫)允許您的測試更快播演,更可重復(fù)冀瓦。
保持?jǐn)?shù)據(jù)存儲作為具有清晰邊界的不同層的一個原因是它允許您延遲選擇特定持久性技術(shù)。 如果您的數(shù)據(jù)存儲是單個類写烤,您可以使用基本持久性策略啟動應(yīng)用程序翼闽,然后稍后升級到SQLite或Core Data,這樣做是有意義的洲炊,而不需要更改應(yīng)用程序代碼庫中的任何其他內(nèi)容感局。
在iOS項目中使用Core Data通常比架構(gòu)本身引發(fā)更多的爭論。 然而暂衡,使用Core Data與VIPER可以是你曾經(jīng)有過的最好的Core Data體驗询微。 Core Data是一個偉大的工具,用于持久化數(shù)據(jù)狂巢,同時保持快速訪問和低內(nèi)存占用撑毛。 但是它有一個習(xí)慣,在所有的應(yīng)用程序的實現(xiàn)文件唧领,特別是他們不應(yīng)該在它的NSManagedObjectContext
卷曲藻雌。 VIPER將Core Data保存在應(yīng)該保存的位置:在數(shù)據(jù)存儲層。
在待辦事項列表示例中(In the to-do list example,)斩个,應(yīng)用程序只知道Core Data正在使用的兩個部分是數(shù)據(jù)存儲本身胯杭,它設(shè)置了Core Data堆棧和數(shù)據(jù)管理器。 (data manager)數(shù)據(jù)管理器執(zhí)行提取請求受啥,將數(shù)據(jù)存儲返回的NSManagedObject
轉(zhuǎn)換為標(biāo)準(zhǔn)PONSO模型對象做个,并將它們傳遞回業(yè)務(wù)邏輯層。 這樣滚局,應(yīng)用程序的核心從不依賴于Core Data居暖,并且作為一個獎勵,你從來不必?fù)?dān)心過時或不良線程的NSManagedObjects
gunking的工作藤肢。
以下是當(dāng)請求訪問Core Data存儲時在數(shù)據(jù)管理器(data manager)中顯示的內(nèi)容:
@implementation 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 typeof(self) welf = self;
[self.dataStore
fetchEntriesWithPredicate:predicate
sortDescriptors:sortDescriptors
completionBlock:^(NSArray* entries) {
if (completionBlock)
{
completionBlock([welf todoItemsFromDataStoreEntries:entries]);
}
}];
}
- (NSArray*)todoItemsFromDataStoreEntries:(NSArray *)entries
{
return [entries arrayFromObjectsCollectedWithBlock:^id(VTDManagedTodoItem *todo) {
return [VTDTodoItem todoItemWithDueDate:todo.date name:todo.name];
}];
}
@end
幾乎與Core Data一樣有爭議的UI Storyboards(故事版)太闺。 Storyboards有許多有用的功能,完全忽略它們將是一個錯誤谤草。 然而跟束,使用Storyboards提供的所有功能很難實現(xiàn)VIPER的所有目標(biāo)莺奸。
我們傾向于做出的妥協(xié)是選擇不使用segue丑孩。 在某些情況下壹瘟,使用segue是有意義的纫事,但segue的危險是它們使得保持屏幕之間的分離以及UI和應(yīng)用邏輯之間的分離是完整的岁经。 根據(jù)經(jīng)驗碟摆,如果實現(xiàn)prepareForSegue方法是必要的凌停,我們盡量不使用segue。
否則慰丛,storyboard是一個偉大的方式來實現(xiàn)您的用戶界面的布局擎析,特別是在使用自動布局。 我們選擇使用Storyboards實現(xiàn)待辦事項列表示例的兩個屏幕轧拄,并使用此類代碼執(zhí)行自己的導(dǎo)航:
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
Using VIPER to Build Modules
通常當(dāng)使用VIPER時揽祥,你會發(fā)現(xiàn)一個屏幕或一組屏幕往往作為一個模塊在一起。 模塊可以用幾種方式描述檩电,但通常最好被認(rèn)為是一個特性拄丰。 在播客應(yīng)用中,模塊可能是音頻播放器或訂閱瀏覽器俐末。 在我們的待辦事項列表應(yīng)用程序中(to-do list app)料按,列表和添加屏幕都是作為單獨(dú)的模塊構(gòu)建的。
將應(yīng)用程序設(shè)計為一組模塊有幾個好處卓箫。 一個是模塊可以具有非常清楚和定義明確的接口载矿,以及獨(dú)立于其他模塊。 這使得添加/刪除功能更容易烹卒,或者更改您的界面向用戶呈現(xiàn)各種模塊的方式闷盔。
我們想在待辦事項列表示例中使模塊之間的分離非常清楚,因此我們?yōu)樘砑幽K定義了兩個協(xié)議甫题。 第一個是模塊接口馁筐,它定義模塊可以做什么。 第二個是模塊委托坠非,它描述了模塊做了什么敏沉。 例:
@protocol VTDAddModuleInterface <NSObject>
- (void)cancelAddAction;
- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;
@end
@protocol VTDAddModuleDelegate <NSObject>
- (void)addModuleDidCancelAddAction;
- (void)addModuleDidSaveAddAction;
@end
由于模塊必須呈現(xiàn)給用戶很有價值,所以模塊的Presenter通常實現(xiàn)模塊接口炎码。 當(dāng)另一個模塊想要呈現(xiàn)這個模塊時盟迟,它的Presenter將實現(xiàn)模塊委托協(xié)議,以便它知道模塊在呈現(xiàn)時做了什么潦闲。
(module)模塊可以包括可以用于多個屏幕的實體攒菠,交互器和管理器的公共應(yīng)用邏輯層。 這當(dāng)然要取決于這些屏幕之間的相互作用以及它們之間的相似性歉闰。 模塊可以很容易地僅表示單個屏幕辖众,如待辦事項列表示例中所示。 在這種情況下和敬,應(yīng)用邏輯層可以非常特定于其特定模塊的行為凹炸。
模塊也只是一個很好的簡單的組織代碼的方式。 保持一個模塊的所有代碼在自己的文件夾中昼弟,并在Xcode中的組啤它,使得它很容易找到,當(dāng)你需要改變的東西。 這是一個偉大的感覺变骡,當(dāng)你找到一個類离赫,你希望找到它。
使用VIPER構(gòu)建模塊的另一個好處是它們變得更容易擴(kuò)展到多個形狀因子塌碌。 為在Interactor層隔離的所有用例提供應(yīng)用程序邏輯渊胸,您可以專注于為平板電腦,手機(jī)或Mac構(gòu)建新的用戶界面台妆,同時重用您的應(yīng)用程序?qū)印?/p>
更進(jìn)一步蹬刷,iPad應(yīng)用程序的用戶界面可能能夠重用iPhone應(yīng)用程序的一些視圖,視圖控制器和演示者频丘。 在這種情況下办成,iPad屏幕將由“超級”演示者和線框表示,這將使用為iPhone編寫的現(xiàn)有演示者和線框構(gòu)成屏幕搂漠。 在多個平臺上構(gòu)建和維護(hù)應(yīng)用程序可能相當(dāng)具有挑戰(zhàn)性迂卢,但是促進(jìn)在模型和應(yīng)用程序?qū)又g重用的良好架構(gòu)有助于簡化這一過程。
Testing with VIPER (用VIPER測試)
以下VIPER鼓勵分離關(guān)注點桐汤,使其更容易采用TDD而克。 Interactor包含獨(dú)立于任何UI的純邏輯,這使得它易于使用測試驅(qū)動怔毛。 Presenter包含準(zhǔn)備顯示數(shù)據(jù)的邏輯员萍,并且獨(dú)立于任何UIKit窗口小部件。 開發(fā)這個邏輯也很容易用測試驅(qū)動拣度。
我們首選的方法是從Interactor開始碎绎。 UI中的一切都是為了滿足用例的需要。 通過使用TDD測試驅(qū)動Interactor的API抗果,您將更好地了解UI與用例之間的關(guān)系筋帖。
例如,我們將看看負(fù)責(zé)即將到來的待辦事項列表的Interactor冤馏。 查找即將到來的項目的政策是找到下周末到期的所有待完成項目日麸,并將每個待完成項目分類為今天,明天逮光,本周或下周代箭。
我們寫的第一個測試是確保Interactor找到下周末到期的所有待完成項目:
- (void)testFindingUpcomingItemsRequestsAllToDoItemsFromTodayThroughEndOfNextWeek
{
[[self.dataManager expect] todoItemsBetweenStartDate:self.today endDate:self.endOfNextWeek completionBlock:OCMOCK_ANY];
[self.interactor findUpcomingItems];
}
一旦我們知道Interactor要求適當(dāng)?shù)拇k事項,我們將寫幾個測試來確認(rèn)它將待辦事項分配到正確的相對日期組(例如今天涕刚,明天等):
- (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];
}
現(xiàn)在我們知道了Interactor的API是什么樣的嗡综,我們可以開發(fā)Presenter。 當(dāng)Presenter從Interactor接收到即將到來的待完成項目時副女,我們將測試我們是否正確地格式化數(shù)據(jù)并在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];
NSDate *dueDate = [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]];
}
我們還想測試當(dāng)用戶想要添加新的待辦事項時蛤高,應(yīng)用程序?qū)舆m當(dāng)?shù)牟僮鳎?/p>
- (void)testAddNewToDoItemActionPresentsAddToDoUI
{
[[self.wireframe expect] presentAddInterface];
[self.presenter addNewEntry];
}
我們現(xiàn)在可以開發(fā)視圖。 當(dāng)沒有即將到來的待辦事項時碑幅,我們要顯示一條特殊消息:
- (void)testShowingNoContentMessageShowsNoContentView
{
[self.view showNoContentMessage];
XCTAssertEqualObjects(self.view.view, self.view.noContentView, @"the no content view should be the view");
}
當(dāng)有即將顯示的待辦事項時戴陡,我們要確保表格正在顯示:
- (void)testShowingUpcomingItemsShowsTableView
{
[self.view showUpcomingDisplayData:nil];
XCTAssertEqualObjects(self.view.view, self.view.tableView, @"the table view should be the view");
}
首先構(gòu)建交互器是與TDD的自然契合。 如果您首先開發(fā)Interactor沟涨,然后是Presenter恤批,那么您將首先構(gòu)建一套圍繞這些層的測試,并為實現(xiàn)這些用例打下基礎(chǔ)裹赴。 您可以對這些類進(jìn)行快速迭代喜庞,因為您不必與UI進(jìn)行交互以測試它們。 然后棋返,當(dāng)你去開發(fā)視圖時延都,你將有一個工作和測試的邏輯和表示層連接到它。 當(dāng)你完成開發(fā)視圖時睛竣,你可能會發(fā)現(xiàn)晰房,第一次運(yùn)行應(yīng)用程序一切正常,因為所有通過的測試告訴你射沟,它將工作殊者。
Conclusion (結(jié)論)
我們希望你喜歡這個介紹VIPER。 你們很多人現(xiàn)在可能想知道下一步去哪里验夯。 如果你想使用VIPER構(gòu)建你的下一個應(yīng)用程序猖吴,你從哪里開始?
本文和我們使用VIPER的應(yīng)用程序示例實現(xiàn)具體和明確定義挥转,因為我們可以使它們海蔽。 我們的待辦事項列表應(yīng)用程序(to-do list app)是相當(dāng)直接,但它也應(yīng)準(zhǔn)確解釋如何使用VIPER構(gòu)建一個應(yīng)用程序绑谣。 在現(xiàn)實世界的項目中准潭,你如何密切關(guān)注這個例子將取決于你自己的一套挑戰(zhàn)和約束。 根據(jù)我們的經(jīng)驗域仇,我們的每個項目都略微改變了使用VIPER的方法刑然,但所有的項目都從使用它來指導(dǎo)他們的方法受益匪淺。
在某些情況下暇务,您希望出于各種原因偏離VIPER設(shè)定的路徑泼掠。 也許你碰到了一個'bunny’objects的戰(zhàn)爭,或者你的應(yīng)用程序?qū)⑹芤嬗谠赟toryboard中使用segue垦细。 沒關(guān)系择镇。 在這些情況下,請考慮VIPER在作出決定時代表的精神括改。 在其核心腻豌,VIPER是基于單一責(zé)任原則的架構(gòu)。 如果你有麻煩,在決定如何前進(jìn)時考慮這個原則吝梅。
您可能還想知道是否可以在您的現(xiàn)有應(yīng)用程序中使用VIPER虱疏。 在這種情況下,考慮使用VIPER構(gòu)建新功能苏携。 我們許多現(xiàn)有的項目都采取了這條路線做瞪。 這允許您使用VIPER構(gòu)建模塊,并且還幫助您發(fā)現(xiàn)任何現(xiàn)有問題右冻,這可能使得更難采用基于單一責(zé)任原則的架構(gòu)装蓬。
開發(fā)軟件的一個偉大的事情是每個應(yīng)用程序是不同的,也有不同的方式來架構(gòu)任何應(yīng)用程序纱扭。 對我們來說牍帚,這意味著每個應(yīng)用程序都是一個學(xué)習(xí)和嘗試新事物的新機(jī)會。 如果你決定嘗試VIPER乳蛾,我們認(rèn)為你會學(xué)到一些新的東西履羞。 謝謝閱讀。
Swift Addendum (Swift附錄)
上周在WWDC蘋果推出了Swift編程語言作為Cocoa和Cocoa Touch開發(fā)的未來屡久。 對于Swift語言形成復(fù)雜的意見還為時過早忆首,但我們知道語言對我們?nèi)绾卧O(shè)計和構(gòu)建軟件有重大影響。 我們決定重寫我們的VIPER TODO示例應(yīng)用程序使用Swift幫助我們了解這對VIPER意味著什么被环。 到目前為止糙及,我們喜歡我們所看到的。 這里有一些Swift的功能筛欢,我們認(rèn)為將提高使用VIPER構(gòu)建應(yīng)用程序的體驗浸锨。
Structs
在VIPER中,我們使用小的版姑,輕量級的模型類在層之間傳遞數(shù)據(jù)柱搜,例如從Presenter到View。 這些PONSO通常旨在簡單地攜帶小量的數(shù)據(jù)剥险,并且通常不打算被子類化聪蘸。 Swift結(jié)構(gòu)體非常適合這些情況。 下面是一個在VIPER Swift示例中使用的結(jié)構(gòu)的示例表制。 注意健爬,這個結(jié)構(gòu)需要是等價的,所以我們重載了==運(yùn)算符來比較它的類型的兩個實例:
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 hasEqualSections
}
Type Safety
也許Objective-C和Swift之間的最大的區(qū)別是如何處理類型么介。 Objective-C是動態(tài)類型娜遵,Swift是非常有意的嚴(yán)格,如何在編譯時實現(xiàn)類型檢查壤短。 對于像VIPER這樣的架構(gòu)设拟,其中應(yīng)用程序由多個不同的層組成慨仿,類型安全性可以是程序員效率和架構(gòu)結(jié)構(gòu)的巨大勝利。 編譯器幫助您確保容器和對象在層邊界之間傳遞時具有正確的類型纳胧。 這是一個使用如上所示的structs的好地方镰吆。 如果一個結(jié)構(gòu)體意圖生活在兩層之間的邊界,那么你可以保證它永遠(yuǎn)不能從這些層之間逃脫由于類型安全躲雅。
建議: 與原文一同觀看,如果您覺得哪里需要修正骡和。請在下方評論相赁。謝謝觀看。