用 VIPER 構建 iOS 應用架構(2)

【編者按】本篇文章由 Jeff Gilbert 和 Conrad Stoll 共同編寫,經過構建一個基礎示例應用,深刻了解 VIPER,並從視圖、交互器等多個部件理清 VIPER 的總體佈局及思路。經過 VIPER 構建 iOS 應用架構,提高應用質量,迎接應用構建的新機遇!本文系 OneAPM 工程師編譯整理,這是本系列的第 2 篇文章。html

用 VIPER 構建 iOS 應用架構(1)ios

UIViewController 的確至關有用。git

在 VIPER 下,視圖控制器會恰當地作好它份內的事——控制視圖。咱們的應用程序有兩個視圖控制器,一個用於列表界面,另外一個用於增長界面。添加視圖控制器的實現是很是基礎的,由於它的功能是控制視圖,代碼以下:github

@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 客戶。交互器能夠彙集來自多個源的數據,提供實現用例的所需信息。而後就看顯示器採集交互器反饋的數據,並格式化用於顯示。數據庫

數據存儲器負責爲交互器提供實體。當交互器應用其業務邏輯時,它將從數據存儲器中檢索實體、操縱實體,而後將更新的實體返回數據存儲器。數據存儲能夠管理實體的持久性,但實體殊不知道數據存儲,所以更不知道如何堅持自身的持久性。編程

交互器也不知道如何將實體持久化。有時交互器可能使用名爲數據管理器的對象類型,以促進與數據存儲器的交互。數據管理器處理多個操做的特定存儲類型,如建立提取請求、創建查詢等。這使得交互器更專一於應用程序的邏輯,而無需知道實體如何彙集或持續。下面的例子就是說明數據管理器的意義。swift

這是示例應用的數據管理器接口:瀏覽器

@interface VTDListDataManager : NSObject

@property (nonatomic, strong) VTDCoreDataStore *dataStore;

- (void)todoItemsBetweenStartDate:(NSDate *)startDate endDate:(NSDate *)endDate completionBlock:(void (^)(NSArray *todoItems))completionBlock;

@end

當使用 TDD 開發交互器時,能切換出生產帶測試雙/模擬的數據存儲器。避免遠程服務器(Web服務)或觸摸盤(數據庫)可使測試更快速,加強其複用性。安全

保持數據存儲做爲有明確界限的獨立層的緣由之一,在於它可讓你推遲選擇一個特定的持久化技術。若是你的數據存儲器是一個單獨的類,你能夠用基本的持久化策略來搭建應用,之後待須要時再升級到 SQLite 或核心數據,而不須要對應用代碼庫進行任何改變。服務器

在 iOS 的項目中使用核心數據每每能激發比架構自己更大的爭議。可是,在 VIPER 中使用核心數據多是最好的核心數據體驗。在持久化數據方面,核心數據是保持快速存取和低內存佔用的絕佳工具。但它有個缺陷:itsNSManagedObjectContext 像觸鬚似的貫穿全部應用的執行文件,特別是在一些它們不該該出現的地方。 VIPER 則能夠保持核心數據出如今正確的地方——數據存儲層。

在待辦事項示例中,僅有應用程序的兩個部件知道核心數據正在使用,其一是數據存儲自己,其中創建核心數據堆棧;其二則是數據管理器。數據管理器執行讀取請求,將數據存儲所返回的 theNSManagedObjects,轉換成標準 PONSO 模型對象,並返回至業務邏輯層。這樣,應用程序的核心再也不依賴核心數據,另外一個好處是,你永遠不用擔憂過去數據或組織很亂的 NSManagedObjects 來破壞你的成果。

當經過請求訪問核心數據存儲時,數據管理器執行以下代碼:

@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

像核心數據同樣引發爭議的是用戶界面故事板。故事板有許多不容忽視的功能。然而,同時採用故事板的全部功能也難以實現 VIPER 的全部目標。

所以,咱們每每退一步選擇不使用 segues。在某些狀況下,使用 segues 是頗有意義的,但伴隨着 segues 的風險,是難以原封不動地保持界面的獨立,以及用戶界面和應用程序邏輯之間的分離。通常來講,若是必須實施 prepareForSegue 方法,咱們最好不採用 segues 。

可是,故事板倒是實現佈局的用戶界面的有效辦法,尤爲在使用自動佈局時。咱們選擇使用故事板來實現待辦事項示例的兩個界面,並用下面的代碼來執行導航:

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)addModuleDidSaveAddAction;

@end

因爲模塊必須展示出來纔有價值,因此模塊的展現器一般實現了模塊接口。當其餘模塊想展現當前模塊時,它的展現器將實現模塊代理協議,所以它知道模塊以前顯示時作了什麼。

一個模塊可能包括實體、交互器、管理器,能夠被用於多個界面的共同應用邏輯層。固然,這取決於界面之間的交互,以及它們是否相似。模塊能夠很容易地在待辦事項示例中展現單個界面。這樣說來,應用邏輯層能夠針對特定模塊的行爲而定製。

模塊也是組織代碼的簡易途徑。將模塊的全部代碼都放在本身的文件夾中,並用 Xcode 分組,便於你在須要時尋找和改動。當你想找的一個類恰好就在你所指望的地方出現時,這種 Feel 倍兒爽!

用 VIPER 構建模塊的另外一個好處是,更容易將其擴展到多個平臺。具備獨立於交互器層的全部用例的應用程序邏輯,經過複用應用程序層,可讓你專一於在平板電腦端、手機端或 Mac 端構建新的用戶界面。

進一步說,適用於 iPad 應用的用戶界面可以重用一些 iPhone 應用的視圖、視圖控制器和控制器。這樣的話,iPad 界面將由「超級」展現器和線框圖來展示,也就是改寫現成的 iPhone 端的展現器和線框構成。構建並維護跨平臺的應用程序至關具備挑戰性,但良好的架構能夠促進模型和應用層的重用,從而讓跨平臺實現容易得多。

用VIPER測試

VIPER 的出現激發了關注點的分離,這使得采用 TDD 變得更加簡便。交互器含有獨立於任何用戶界面的純邏輯,測試起來更加容易。展現器包含用於顯示準備數據的邏輯,而且獨立於任何 UIKit 部件。開發這種邏輯也便於測試。

咱們的首選方法是從交互器開始。UI 中的一切是服務於用例的需求。經過使用 TDD 來測試交互器的 API,你能夠更好地瞭解用戶界面和用例之間的關係。

例如,咱們着眼於交互器負責的待辦事項列表。尋找新的列表的策略是,要找到全部截止於下週末的待辦事項,並將每一個待辦事項歸類爲到期日是今天、明天、本週晚些時候或下週。

爲確保交互器找到截止於下週末的全部待辦事項,咱們編寫第一個測試:

- (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 的樣子,就能夠開發展現器。當展現器收到來自交互器的待辦事項,咱們將測試是否恰當地格式化數據,並在用戶界面中顯示:

- (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]];
}

同時,咱們也想測試,當用戶想增長一個新的待辦事項時,應用程序是否能正確的啓動響應操做:

- (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 所指定的道路。也許你遇到了不少「bunny」對象,或者你的應用程序將受益於在故事板中使用 segues。不要緊,在這種狀況下,在作出決定時想想 VIPER 所表明的精神。它的核心始終是:基於單一責任原則的架構。若是遇到問題,在決定如何向前推動時想一想這個原則。

你可能想知道在現有應用中是否能使用 VIPER。遇到這種狀況,你能夠考慮用 VIPER 建一個新功能,許多項目都採起了這種方法。這能讓你用 VIPER 構建模塊,幫助你發現許多創建在單一責任原則基礎上形成難以運用架構的問題。

開發軟件的最大挑戰在於,每一個應用都迥然不一樣,應用程序的架構方式也不同。對咱們來講,這意味着每一個應用程序都是學習和嘗試新事物的機遇。若是你決定嘗試 VIPER,你也會受益不淺。

Swift 補充

不久前,在 WWDC 上蘋果推出了 Swift 編程語言,這將成爲 Cocoa 和 Cocoa Touch 開發的將來。如今評判 Swift 語言還太早,但咱們知道,語言與咱們如何設計、構建軟件息息相關。咱們決定用 Swift 改寫 VIPER TODO 示例應用,幫助咱們瞭解 Swift 對 VIPER 的意義。迄今爲止,咱們確實有所收穫。如下是 Swift 的幾個特色,可能會改善用 VIPER 開發應用程序的體驗。

結構體

在 VIPER 中,咱們採用小型的、輕量化、模型類來傳遞層之間的數據,好比展現器到視圖。這些 PONSOs 一般只是簡單地採起少許數據,而且這些類一般不會被繼承。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 = 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
}

類型安全

或許,Objective-C 和 Swift 之間最大的區別在於類型處理上的不一樣。 Objective-C 是動態類型,而 Swift 在編譯中對實現類型檢查時很是嚴格。對於像 VIPER 的架構,當一個應用程序由多個不一樣層構成,類型安全對開發者效率和構架結構來講都是巨大的優點。編譯器幫助你確保在層邊界傳遞時,容器和對象始終是正確的類型。由上文可知,這即是使用結構體的最佳位置。若是一個結構體能在兩層之間的邊界保駕護航,因爲類型安全的限制,你就能保證它永遠沒法逃離邊界。

(完結)

用 VIPER 構建 iOS 應用架構(1)

原文地址:Architecting iOS Apps with VIPER

本文系 OneAPM 工程師編譯整理。OneAPM 是應用性能管理領域的新興領軍企業,能幫助企業用戶和開發者輕鬆實現:緩慢的程序代碼和 SQL 語句的實時抓取。想閱讀更多技術文章,請訪問 OneAPM 官方博客

相關文章
相關標籤/搜索