VIPER 架構(gòu)
VIPER單詞是
View
壹哺,Interactor
蛮拔,Presenter
乖订,Entity
和Routing
的反義詞墅诡。Clean Architecture
將應(yīng)用程序的邏輯結(jié)構(gòu)劃分為不同的責(zé)任層。這使得隔離依賴關(guān)系(例如您的數(shù)據(jù)庫(kù))以及在層之間的邊界處測(cè)試交互更容易
VIPER的主要部分是:
-
View
:顯示演示者告訴的內(nèi)容,并將用戶輸入中繼回演示者。 -
Interactor
:包含用例指定的業(yè)務(wù)邏輯歉甚。 -
Presenter
:包含視圖邏輯,用于準(zhǔn)備要顯示的內(nèi)容(從Interactor接收)并響應(yīng)用戶輸入(通過從Interactor請(qǐng)求新數(shù)據(jù))扑眉。 -
Entity
:包含交互器使用的基本模型對(duì)象纸泄。 -
Routing
:包含導(dǎo)航邏輯,用于描述按什么順序顯示哪些屏幕襟雷。
這種分離也符合單一責(zé)任原則
, Interactor
對(duì)業(yè)務(wù)分析師負(fù)責(zé)刃滓,Presenter
代表交互設(shè)計(jì)師仁烹,而View
對(duì)視覺設(shè)計(jì)師負(fù)責(zé)耸弄。
Interactor
包含指定的業(yè)務(wù)邏輯
Interactor
代表應(yīng)用程序中的單個(gè)用例。它包含操縱模型對(duì)象Entities(實(shí)體)以執(zhí)行特定任務(wù)的業(yè)務(wù)邏輯卓缰。在Interactor
中完成的業(yè)務(wù)邏輯應(yīng)獨(dú)立于任何UI计呈。可以在iOS應(yīng)用程序或OS X應(yīng)用
程序中使用相同的Interactor
征唬。
因?yàn)?code>Interactor是普通的PONSO (Plain Old NSObject) 捌显,所以使用TDD進(jìn)行開發(fā)很容易。
Eg:
- (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實(shí)體
實(shí)體是由Interactor操縱的實(shí)體對(duì)象总寒。實(shí)體僅由Interactor操縱扶歪。Interactor從不將實(shí)體傳遞到表示層(即Presenter)。
實(shí)體也往往是PONSO摄闸。如果您使用的是Core Data善镰,則希望您的托管對(duì)象保留在數(shù)據(jù)層后面。交互器不應(yīng)與一起使用NSManagedObjects年枕。
這是我們待辦事項(xiàng)的實(shí)體:
@interface VTDTodoItem : NSObject
@property (nonatomic, strong) NSDate* dueDate;
@property (nonatomic, copy) NSString* name;
+ (instancetype)todoItemWithDueDate:(NSDate*)dueDate name:(NSString*)name;
@end
Presenter
Presenter 是一個(gè)PONSO炫欺,主要是由驅(qū)動(dòng)UI的邏輯組成,它知道何時(shí)顯示用戶界面熏兄。它從用戶交互中收集輸入品洛,以便它可以更新UI并將請(qǐng)求發(fā)送到Interactor。當(dāng)用戶點(diǎn)擊+按鈕添加新的待辦事項(xiàng)時(shí)摩桶,addNewEntry將被調(diào)用桥状。對(duì)于此操作,演示者要求線框呈現(xiàn)用于添加新項(xiàng)目的UI
- (void)addNewEntry
{
[self.listWireframe presentAddInterface];
}
Presenter還從Interactor接收結(jié)果硝清,并將結(jié)果轉(zhuǎn)換為有效顯示在視圖中的形式岛宦。
- (void)foundUpcomingItems:(NSArray*)upcomingItems
{
if ([upcomingItems count] == 0)
{
[self.userInterface showNoContentMessage];
}
else
{
[self updateUserInterfaceWithUpcomingItems:upcomingItems];
}
}
實(shí)體永遠(yuǎn)不會(huì)從Interactor傳遞給Presenter。而是將沒有行為的簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu)從Interactor傳遞給Presenter耍缴。這樣可以防止在Presenter中完成任何“實(shí)際工作”砾肺。演示者只能準(zhǔn)備要在視圖中顯示的數(shù)據(jù)挽霉。
View(視圖)
視圖是被動(dòng)的。它等待演示者將其內(nèi)容顯示出來(lái)变汪。它從不要求演示者提供數(shù)據(jù)侠坎。為視圖定義的方法(例如,用于登錄屏幕的LoginView)應(yīng)允許Presenter進(jìn)行更高級(jí)別的抽象交流裙盾,以其內(nèi)容表示实胸,而不是該內(nèi)容的顯示方式。演示者不知道的存在UILabel番官,UIButton等等庐完。主持人只知道它維護(hù)的內(nèi)容以及何時(shí)應(yīng)該顯示它。由視圖決定內(nèi)容的顯示方式徘熔。
View是一個(gè)抽象接口门躯,在Objective-C中使用協(xié)議進(jìn)行了定義。一個(gè)UIViewController或其中一個(gè)子類將實(shí)現(xiàn)View協(xié)議酷师。例如讶凉,示例中的“添加”屏幕具有以下界面:
@protocol VTDAddViewInterface <NSObject>
- (void)setEntryName:(NSString *)name;
- (void)setEntryDueDate:(NSDate *)date;
@end
當(dāng)用戶點(diǎn)擊“取消”按鈕時(shí),視圖控制器將告訴該事件處理程序該用戶已指示應(yīng)取消添加操作山孔。這樣懂讯,事件處理程序可以解決關(guān)閉添加視圖控制器并告知列表視圖進(jìn)行更新的問題。
The boundary between the View and the Presenter is also a great place for ReactiveCocoa. In this example, the view controller could also provide methods to return signals that represent button actions. This would allow the Presenter
to easily respond to those signals without breaking separation of responsibilities.
View和Presenter之間的邊界也是ReactiveCocoa的好地方台颠。在此示例中褐望,視圖控制器還可以提供返回表示按鈕操作的信號(hào)的方法。這將使演示者可以輕松響應(yīng)這些信號(hào)串前,而不會(huì)破壞職責(zé)分離瘫里。
Routing(路由)
從一個(gè)屏幕到另一個(gè)屏幕的路線是由交互設(shè)計(jì)器創(chuàng)建的wireframes定義的。在VIPER中酪呻,路由的職責(zé)在兩個(gè)對(duì)象(Presenter和wireframes)之間共享减宣。wireframes對(duì)象擁有的UIWindow,UINavigationController玩荠,UIViewController等漆腌,這是負(fù)責(zé)創(chuàng)建一個(gè)視圖/視圖控制器,并在窗口安裝阶冈。
由于Presenter包含對(duì)用戶輸入做出反應(yīng)的邏輯闷尿,因此Presenter知道何時(shí)導(dǎo)航到另一個(gè)屏幕以及導(dǎo)航到哪個(gè)屏幕。同時(shí)女坑,wireframes知道如何導(dǎo)航填具。因此,Presenter將使用wireframes執(zhí)行導(dǎo)航。它們共同描述了從一個(gè)屏幕到下一個(gè)屏幕的路由劳景。
wireframes也是處理導(dǎo)航過渡動(dòng)畫的明顯位置誉简。從添加wireframes查看以下示例:
@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
VIPER配套的應(yīng)用組件
iOS應(yīng)用程序體系結(jié)構(gòu)需要考慮以下事實(shí):UIKit和Cocoa Touch是應(yīng)用程序基于其構(gòu)建的主要工具。架構(gòu)需要與應(yīng)用程序的所有組件和平共處盟广,但是還需要提供有關(guān)框架的某些部分如何使用以及它們?cè)谀睦锞幼〉闹改稀?/p>
iOS應(yīng)用的主力軍是UIViewController闷串。很容易假設(shè)競(jìng)爭(zhēng)者取代MVC會(huì)避免大量使用視圖控制器。但是視圖控制器是該平臺(tái)的核心:它們可以處理方向變化筋量,響應(yīng)用戶輸入烹吵,與導(dǎo)航控制器等系統(tǒng)組件很好地集成在一起,并且現(xiàn)在與iOS 7集成在一起桨武,可以在屏幕之間進(jìn)行自定義轉(zhuǎn)換肋拔。它們非常有用。
使用VIPER呀酸,視圖控制器可以完全實(shí)現(xiàn)其意圖:控制視圖凉蜂。我們的待辦事項(xiàng)列表應(yīng)用程序有兩個(gè)視圖控制器,一個(gè)用于列表屏幕七咧,一個(gè)用于添加屏幕跃惫。添加視圖控制器的實(shí)現(xiàn)非扯L荆基礎(chǔ)艾栋,因?yàn)樗龅木褪强刂埔晥D:
@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ò)時(shí),它們通常更具吸引力蛉顽。但是蝗砾,該聯(lián)網(wǎng)應(yīng)該在哪里進(jìn)行,啟動(dòng)它的責(zé)任是什么携冤?通常由Interactor來(lái)啟動(dòng)網(wǎng)絡(luò)操作悼粮,但是它不會(huì)直接處理網(wǎng)絡(luò)代碼。它將詢問依賴項(xiàng)曾棕,例如網(wǎng)絡(luò)管理器或API客戶端扣猫。交互器可能必須聚合來(lái)自多個(gè)源的數(shù)據(jù),以提供實(shí)現(xiàn)用例所需的信息翘地。然后申尤,由演示者決定Interactor返回的數(shù)據(jù)并將其格式化以進(jìn)行演示。
數(shù)據(jù)存儲(chǔ)負(fù)責(zé)向交互器提供實(shí)體衙耕。當(dāng)Interactor應(yīng)用其業(yè)務(wù)邏輯時(shí)昧穿,它將需要從數(shù)據(jù)存儲(chǔ)中檢索實(shí)體,操縱實(shí)體橙喘,然后將更新后的實(shí)體放回?cái)?shù)據(jù)存儲(chǔ)中时鸵。數(shù)據(jù)存儲(chǔ)區(qū)管理實(shí)體的持久性。實(shí)體不知道數(shù)據(jù)存儲(chǔ)厅瞎,因此實(shí)體不知道如何持久保存自己饰潜。
Interactor也不應(yīng)該知道如何持久化實(shí)體初坠。有時(shí),Interactor可能希望使用一種稱為數(shù)據(jù)管理器的對(duì)象來(lái)促進(jìn)其與數(shù)據(jù)存儲(chǔ)的交互彭雾。數(shù)據(jù)管理器處理更多特定于商店的操作類型某筐,例如創(chuàng)建訪存請(qǐng)求,構(gòu)建查詢等冠跷。這使Interactor可以將更多的精力放在應(yīng)用程序邏輯上南誊,而不必了解有關(guān)實(shí)體如何收集或持久化的任何信息。當(dāng)您使用Core Data時(shí)蜜托,一個(gè)使用數(shù)據(jù)管理器的例子如下所述抄囚。
這是示例應(yīng)用程序的數(shù)據(jù)管理器的界面:
@interface VTDListDataManager : NSObject
@property (nonatomic, strong) VTDCoreDataStore *dataStore;
- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;
@end
使用TDD開發(fā)Interactor時(shí),可以通過測(cè)試double / mock切換生產(chǎn)數(shù)據(jù)存儲(chǔ)橄务。不與遠(yuǎn)程服務(wù)器通信(對(duì)于Web服務(wù))或觸摸磁盤(對(duì)于數(shù)據(jù)庫(kù))可以使測(cè)試更快幔托,更可重復(fù)。
保持?jǐn)?shù)據(jù)存儲(chǔ)為具有清晰邊界的不同層的一個(gè)原因是蜂挪,它允許您延遲選擇特定的持久性技術(shù)重挑。如果您的數(shù)據(jù)存儲(chǔ)是單個(gè)類,則可以使用基本的持久性策略啟動(dòng)應(yīng)用程序棠涮,然后在需要時(shí)再升級(jí)到SQLite或Core Data谬哀,而無(wú)需更改應(yīng)用程序代碼庫(kù)中的任何其他內(nèi)容。
在iOS項(xiàng)目中使用Core Data通常會(huì)引發(fā)比架構(gòu)本身更多的爭(zhēng)論严肪。但是史煎,將核心數(shù)據(jù)與VIPER一起使用可能是您曾經(jīng)擁有的最佳核心數(shù)據(jù)體驗(yàn)。核心數(shù)據(jù)是保持?jǐn)?shù)據(jù)同時(shí)保持快速訪問和低內(nèi)存占用的絕佳工具驳糯。但是它習(xí)慣于在NSManagedObjectContext整個(gè)應(yīng)用的實(shí)現(xiàn)文件中(尤其是不應(yīng)該存在的地方)都卷須篇梭。VIPER將核心數(shù)據(jù)保留在應(yīng)有的位置:在數(shù)據(jù)存儲(chǔ)層。
在待辦事項(xiàng)列表示例中酝枢,應(yīng)用程序僅知道正在使用Core Data的兩個(gè)部分是數(shù)據(jù)存儲(chǔ)庫(kù)本身(用于設(shè)置Core Data堆棧)和數(shù)據(jù)管理器恬偷。數(shù)據(jù)管理器執(zhí)行獲取請(qǐng)求,將NSManagedObjects數(shù)據(jù)存儲(chǔ)返回的結(jié)果轉(zhuǎn)換為標(biāo)準(zhǔn)PONSO模型對(duì)象帘睦,然后將其傳遞回業(yè)務(wù)邏輯層袍患。這樣,應(yīng)用程序的核心永遠(yuǎn)不會(huì)依賴于核心數(shù)據(jù)官脓,此外协怒,您也不必?fù)?dān)心陳舊或處理不佳的線程N(yùn)SManagedObjects。
發(fā)出訪問核心數(shù)據(jù)存儲(chǔ)的請(qǐng)求時(shí)卑笨,數(shù)據(jù)管理器內(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
UI Storyboard幾乎與Core Data一樣引起爭(zhēng)議孕暇。故事板具有許多有用的功能,而完全忽略它們將是一個(gè)錯(cuò)誤。但是妖滔,在利用情節(jié)提要板提供的所有功能時(shí)隧哮,很難實(shí)現(xiàn)VIPER的所有目標(biāo)。
我們傾向于做出的妥協(xié)是選擇不使用segues座舍。在某些情況下沮翔,使用segue是有意義的,但是segue的危險(xiǎn)在于它們很難使屏幕之間以及UI和應(yīng)用程序邏輯之間的分隔保持完整曲秉。根據(jù)經(jīng)驗(yàn)采蚀,如果需要執(zhí)行prepareForSegue方法,我們盡量不要使用segues承二。
否則榆鼠,情節(jié)提要是實(shí)現(xiàn)用戶界面布局的好方法,尤其是在使用“自動(dòng)布局”時(shí)亥鸠。我們選擇使用情節(jié)提要來(lái)實(shí)現(xiàn)待辦事項(xiàng)清單示例的兩個(gè)屏幕妆够,并使用諸如此類的代碼來(lái)執(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
使用VIPER構(gòu)建模塊
通常在使用VIPER時(shí),您會(huì)發(fā)現(xiàn)一個(gè)屏幕或一組屏幕傾向于作為一個(gè)模塊組合在一起负蚊∩衩茫可以用幾種方法來(lái)描述模塊,但是通常最好將其視為功能家妆。在播客應(yīng)用中鸵荠,模塊可以是音頻播放器或訂閱瀏覽器。在我們的待辦事項(xiàng)列表應(yīng)用程序中揩徊,列表和添加屏幕分別構(gòu)建為單獨(dú)的模塊腰鬼。
將應(yīng)用程序設(shè)計(jì)為一組模塊有一些好處嵌赠。一是模塊可以具有非常清晰和定義明確的界面塑荒,并且可以獨(dú)立于其他模塊。這使得添加/刪除功能或更改界面向用戶呈現(xiàn)各種模塊的方式變得更加容易姜挺。
我們希望在待辦事項(xiàng)清單示例中使模塊之間的分隔非常清楚齿税,因此我們?yōu)閍dd模塊定義了兩種協(xié)議。第一個(gè)是模塊接口炊豪,它定義模塊可以執(zhí)行的操作凌箕。第二個(gè)是模塊委托,它描述了模塊的工作词渤。例:
@protocol VTDAddModuleInterface <NSObject>
- (void)cancelAddAction;
- (void)saveAddActionWithName:(NSString *)name dueDate:(NSDate *)dueDate;
@end
@protocol VTDAddModuleDelegate <NSObject>
- (void)addModuleDidCancelAddAction;
- (void)addModuleDidSaveAddAction;
@end