你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> MVVM without ReactiveCocoa

MVVM without ReactiveCocoa

編輯:IOS開發基礎

u=500250691,3617306368&fm=15&gp=0.jpg

MVVM 是 MVC 模式的一種演進,它主要解決了 ViewController 過於臃腫帶來的不易維護和測試的問題。其中 ViewModel 的主要職責是處理業務邏輯並提供 View 所需的數據,這樣 VC 就不用關心業務,自然也就瘦了下來。ViewModel 只關心業務數據不關心 View,所以不會與 View 產生耦合,也就更方便進行單元測試。

View 是一個殼,它所呈現的內容都需要由 ViewModel 來提供,而 View 又不與 ViewModel 直接溝通,這時就需要 ViewController 來做中間的協調者。

ViewController 持有 View 和 ViewModel,當 VC 初始化時,會讓 ViewModel 去取數據,簡單來說就是調用 VM 的某個獲取數據的方法。

使用 MVVM 最舒服的姿勢是搭配 ReactiveCocoa。不過它的問題在於學習成本和維護成本比較高,在小團隊中或許還可以嘗試,當開發人員數量較多時就很難推起來了。這也是我們今天要講的主題:如何不借助 ReactiveCocoa 來實現 MVVM。

先從數據的獲取開始說起吧。在 ReactiveCocoa 裡有一個類叫「RACCommand」,它的主要作用是執行某個會改變數據的操作,然後提供獲取數據的方法,跟我們想要達到的目的很像,所以可以借鑒這個思路,寫一個簡單的 Command。

typedef void(^MGJCommandCompletionBlock)(id error, id content);

// 1
typedef void(^MGJCommandConsumeBlock)(id input, MGJCommandCompletionBlock completionHandler);

// 2
typedef void(^MGJCommandCancelBlock)();

@interface MGJCommandResult : NSObject
// 3
@property (nonatomic) NSError *error;
// 4
@property (nonatomic) id content;
@end

@interface MGJCommand : NSObject

// 5
@property (nonatomic, readonly) BOOL executing;
// 6
@property (nonatomic, readonly) MGJCommandResult *result;
- (instancetype)initWithConsumeHandler:(MGJCommandConsumeBlock )consumeHandler;
// 7

- (instancetype)initWithConsumeHandler:(MGJCommandConsumeBlock )consumeHandler cancelHandler:(MGJCommandCancelBlock )cancelHandler;
// 8
- (void)execute:(id)input;
// 9
- (void)cancel;
@end
  1. input 是外部傳過來的值,比如 user_id,當拿到數據後,調用下 completionHandler,這樣 result 屬性就會變化。

  2. 有些操作,如 http 請求,需要手動取消。

  3. 單獨把 error 作為一個屬性放出來,是因為很多數據請求操作都可能出錯,當出錯後,只需改變這個 error 屬性即可。

  4. content 存放了這個 Command 的數據處理結果。

  5. 標識了這個 Command 目前的運行狀態,比如可以根據這個狀態來顯示 loading。

  6. 每次 Command 執行完一個任務後,result 都會改變,外部可以 KVO 這個 result,然後就可以實時獲取最新的結果了。

  7. Command 的執行邏輯,如果實現了 cancelHandler 的話,外部調用 cancel,這個 Handler 就會被觸發。

  8. 外部可以調用這個方法來觸發 Command 的執行,同時可以傳一個參數進來。

  9. 外部可以調用這個方法來取消 Command 的執行。

實現起來也蠻簡單的,這裡就不多說了。用起來大概是這樣:

// SomeViewModel.m

@weakify(self);
self.followCommand = [[MGJCommand alloc] initWithConsumeHandler:^(id input, MGJCommandCompletionBlock completionHandler) {
    @strongify(self);
    [FollowRequest getFollowList:(NSDictionary *)input success:^(NSArray *users) {
        self.usersToFollow = users;
        completionHandler(nil, kFollowExpertSearchSucceedSignal);
    } failure:^(StatusEntity *error) {
        completionHandler(error, nil);
    }];
}];

在 ViewController 裡的用法大概像這樣

// SomeViewController.m

- (void)didTapFollowButton:(UIButton *)button
{
    // 根據 button 找到 userID
    [self.viewModel.followCommand execute:userID];
}

就是這樣,VC 本身不處理業務邏輯,都交給 ViewModel 去處理,而這些數據請求的結果處理又有不同的處理方式。

Delegate

當 ViewModel 拿到數據後,可以把結果以 Delegate 的方式通知 VC,就像這樣

// SomeViewController.m

- (void)didFollowUserWithResult:(id)result
{
    self.followButton.enabled = YES;
    [self.followButton doSomeAnimation];
}

這樣做的好處是比較符合蘋果既有的設計模式,而且也可以通過查看 Delegate 協議來知道 VM 暴露了哪些接口供外部使用。

不過這種方法少了點靈活性,比如需要聯合多個屬性的變化來做一些事情時,處理起來就會比較麻煩,這也是 RAC 強大的地方。

KVO

RAC 是基於 KVO 構建的,所以也可以用 KVO 來讓 VC 獲取 VM 的變化。

但我們都知道 KVO 的槽點比較多,比如使用起來不方便,用完還要記得移除等。這裡可以使用 Facebook 開源的 KVOController,它比較好的處理了 KVO 存在的一些問題,同時又能發揮 KVO 帶來的便捷性。

有了它我們就能在一個地方把 VM 的更新處理掉了

- (void)handleViewModelUpdate
{
    [self.KVOController observe:self.viewModel keyPath:@"followCommand.result" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(id observer, id target, NSDictionary *change) {
        id newValue = change[NSKeyValueChangeNewKey];
        // doSomething with the newValue
    }];
    
    // 對 VM 其他 keyPath 的處理也都可以放到這裡
    
}

如果覺得這樣的寫法還是太麻煩,可以做一層簡單的封裝,使用起來就像這樣:

- (void)handleViewModelUpdate
{
    [self observe:self.viewModel keyPath:@"followCommand.result" block: ^(id newValue){
        // use newValue to update view
    }];
}

是不是會好一點,使用 KVO 比 Delegate 好的一點是不用再額外聲明協議和方法,而且支持 block,使用起來也會方便些。

對於像 error 這樣很多操作都會產生同樣結果的場景,可以單獨拿出來,作為 ViewModel 的一個屬性,使用時,直接 KVO 這個屬性即可。

細節處理

如果不涉及到 TableView 等會出現復用場景的地方,MVVM 使用起來還是比較方便的。如果有了 TableView,又要做一些額外的處理。

一般來說,VC 可以帶一個 VM,那如果出現 Cell 時怎麼辦,Cell 裡又包含了按鈕,按鈕又需要數據請求又怎麼處理?這些都是比較常見的場景,也可以通過 MVVM 來解決。

我們知道 VM 的職責是為 View 提供數據支持,Cell 也是一個 View,那麼為 Cell 配備一個 VM 不就可以了麼。

這樣的話,VC 的 VM 需要包含一個數組,裡面的元素是 CellVM,使用起來就像這樣

// SomeViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
    cell.viewModel = self.viewModel.cellViewModels[indexPath.row];
    
    // cell 可能會用到 cellVM 裡的 Command,所以在這裡處理 command 的執行結果
    [self observe:cell keyPath:@"likeCommand.result" block: ^(id newValue){
        // update cell after like
    }];
    
    return cell
    
}

當然僅僅如此是不夠的,我們需要找個恰當的時機把 KVO 移除,避免多次監聽。UITableViewDelegate 裡的這個方法就很適合。

// SomeViewController.m

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    [self unobserve:cell keyPath:@"likeCommand.result"];
}

不過這裡也要講究一個平衡,如果 Cell 的類型比較多,且涉及 Command 的地方不多,只是做呈現方面的工作,直接使用 Entity 會更方便。

Tips

  • ViewController 盡量不涉及業務邏輯,讓 ViewModel 去做這些事情。

  • ViewController 只是一個中間人,接收 View 的事件、調用 ViewModel 的方法、響應 ViewModel 的變化。

  • ViewModel 不能包含 View,不然就跟 View 產生了耦合,不方便復用和測試。

  • ViewModel 之間可以有依賴。

  • ViewModel 避免過於臃腫,不然維護起來也是個問題。

MVVM 並不復雜,跟 MVC 也是兼容的,只是多了一個 ViewModel 層,但就是這麼一個小改動,就能讓代碼變得更加容易閱讀和維護,不妨試一下吧。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved