從 C-41 看 MVVM 和 ReactiveCocoa

從 C-41 看 MVVM 和 ReactiveCocoa

基本概念

C-41 是一個關於 MVVMReactiveCocoa 的開源程序,我是經過 objc.io 上的一篇文章知道它的,相關地址:html

MVVM(Model-View-ViewModel) 和 RAC(ReactiveCocoa) 都有不錯的介紹文章,前面提到的是一篇,其餘的附在文章結尾介紹給你們。react

閱讀這篇文章是須要一點 MVVM 和 RAC 的基礎的,徹底不知道什麼是 MVVM 或 RAC 的同窗請先了解它們。git

據我觀察,MVVM 基本上是這麼用的:一個 View/ViewController 對應一個 ViewModel,一個 ViewModel 一般只對應一個 Model,不過也可能聚合多個 Model(在這個程序中未出現)。若是一個 View/ViewController 想要對應不僅一個 ViewModel,那就說明這個 View/ViewController 須要拆分紅更細的部分,由更細的部分各自持有更細的 ViewModel。github

文章差很少是按照個人代碼閱讀順序寫的,不過按照對 RAC 的使用深度稍微調整了一下。編程

啓動流程

ASHAppDelegate 中,初始化了自定義的 CoreData 棧 ASHCoreDataStack,併爲 ASHMasterViewController設置了 ViewModel。架構

這個程序中的 Model 所有都是依託於 CoreData 的數據類型,其實就兩個 ASHRecipeASHStep框架

ASHMasterViewController 的 ViewModel 做爲 ASHMasterViewModel 的實例,繼承自 RVMViewModel,這是一個第三方爲 RAC(ReactiveCocoa)提供的 ViewModel 基類,可使用 CocoaPods 集成到項目裏。 RVMViewModel 假定一個 ViewModel 只對應一個 Model。mvvm

而後程序就進入 ASHMasterViewController 的控制範圍。佈局

ASHMasterViewControllerASHMasterViewModel

這個 ViewController 持有一個做爲 Public 屬性的 ViewModel, ASHMasterViewModel動畫

咱們看到,ViewController 裏要顯示什麼數據,都是直接從 self.viewModel 裏直接取,並無作額外的處理,這使得 ViewController 瘦了不少,專一於處理 View 層的事情(輸入相應、界面佈局和動畫等等)。

值得一提的是,在 ViewDidLoad 裏,綁定了 ViewModel 的 updatedContentSignal 到一個 Block,@weakify@strongify 來自 libextobjc,用於解決 Block 引用的內存泄露問題,RAC 已經自帶這個 Pod。至於這兩個宏具體生成什麼代碼,能夠看文末附註。

@weakify(self);
[self.viewModel.updatedContentSignal subscribeNext:^(id x) {
    @strongify(self);
    [self.tableView reloadData];
}];

另外這幾行代碼的意思是若是信號 self.viewModel.updatedContentSignal 觸發 next 事件並返回值,那麼執行 subscribeNext 對應的 Block 代碼。

而 ViewModel 的 updatedContentSignal 是咱們在 ASHMasterViewModel 中自定義的信號:

@property (nonatomic, strong) RACSubject *updatedContentSignal;

咱們在代碼裏手動觸發這個信號的 next 事件:

[(RACSubject *)self.updatedContentSignal sendNext:nil];

基本上這是一個比較標準的 TableViewController 子類,沒有太多額外的內容。

接下來有幾種方式跳轉到其餘 ViewController:

  • - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
  • - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender

無一例外,都是初始化了對應的 ViewController,而後設置它的 ViewModel。不過這裏值得注意的是,下一層級的 ViewController 的 ViewModel,是由這一層級的 ViewController 的 self.viewModel 獲取的。

ASHEditRecipeViewControllerASHEditRecipeViewModel

ASHEditRecipeViewController 又是一個 TableViewController,在 viewDidLoad 裏有這麼一句:

// ReactiveCocoa Bindings
RAC(self, title) = RACObserve(self.viewModel, name);

這就是爲何 MVVM 常常和 ReactiveCocoa 一塊兒用的緣由之一了,View 一般須要觀察 ViewModel 的變化,在 ViewModel 變化的時候,自動更改 View 裏的對應部分。這裏就是讓 self.titile 自動反應 self.viewModel.name 的變化。

另外在 -(void)configureTitleCell:(ASHTextFieldCell *)cell forIndexPath:(NSIndexPath *)indexPath 裏有這麼一句:

RAC(self.viewModel, name) = [cell.textField.rac_textSignal takeUntil:cell.rac_prepareForReuseSignal];

咱們發現賦值等號的右邊不是用 RACObserve 建立的Signal,而是使用 ReactiveCocoatextField 作的擴展 rac_textSignal, 它其實是建立了一個監聽 textFieldUIControlEventEditingChanged 事件的信號。 takeUntil:cell.rac_prepareForReuseSignal 則是指只有當 cell-prepareForReuse被調用時才觸發這個信號的 nextcompleted 事件。

ViewController 的其餘部分一切如常,接下來咱們看看 ASHEditRecipeViewModel

-(instancetype)initWithModel:(id)model 這個方法裏有個RACChannelTo,這是幹什麼的呢?

RACChannelTo(self, name) = RACChannelTo(self.model, name);
RACChannelTo(self, blurb) = RACChannelTo(self.model, blurb);
RACChannelTo(self, filmType, @(ASHRecipeFilmTypeColourNegative)) = RACChannelTo(self.model, filmType, @(ASHRecipeFilmTypeColourNegative));

RACChannelTo(self, name) = RACChannelTo(self.model, name); 這種寫法是個雙向綁定,也就是 self.name 改變,self.model.name 會改變;反之 self.model.name 改變的話,self.name 也會改變。

RACChannelTo(self, filmType, @(ASHRecipeFilmTypeColourNegative)) 裏面第三個參數是指,若是值的變化中出現 nil,那麼就會使用這個值來代替,至關於一個默認值。

這是爲何 MVVM 一般會依賴 ReactiveCocoa 的緣由之二,即 ViewModel 和 Model 的改變一般是須要雙向同步的。

ASHDetailViewControllerASHDetailViewModel

ASHDetailViewController 沒什麼好說的,咱們看 ASHDetailViewModel

RAC(self, canStartTimer) = [RACObserve(self.model, steps) map:^id(NSOrderedSet *value) {
	return @([value count] > 0);
}];

這裏出現了 map,對一個信號執行 map 其實就是經過映射改變了它信號流下一步的值,即再也不是原來 Observe 到的值。這裏原先 Observe 到的值是 self.model.steps,是一個 NSOrderedSet,如今通過map,信號流的下一步收到的輸入就是一個封裝成 NSNumber的 BOOL 值,因而就和 self.canStartTimer 對應起來了。這裏信號流的概念就和 Unix 管道比較像,這一點應該在其餘介紹 RAC響應式編程 的文章中有所說起。

ASHTimerViewControllerASHTimerViewModel

ASHTimerViewController 一樣沒什麼好看的,咱們看 ASHTimerViewModel

RAC(self, nextStepString) = [RACSignal combineLatest:@[RACObserve(self.model, steps), RACObserve(self, currentStepIndex)]
                                              reduce:^id(NSOrderedSet *steps, NSNumber *currentStepIndexNumber) {
    NSInteger nextStepIndex = [currentStepIndexNumber integerValue] + 1;
    if (nextStepIndex >= 0 && nextStepIndex < steps.count) {
        return [[steps objectAtIndex:nextStepIndex] name];
    } else {
        return @"";
    }
}];

咱們發現一個屬性不只僅只能綁定由單個值改變觸發的信號,還能夠綁定由多個值改變觸發的聚合信號。經過 combineLatest:reduce: 咱們能夠聚合多個信號成一個信號,讓屬性的改變是依賴多個值的變化的。

結尾

看到這裏就差很少了,RAC 有不少高級的特性,MVVM 也有一些更復雜的實現方式,而這個程序僅使用了比較基本的 MVVM 結構和 RAC 特性來構建,對於剛剛接觸 MVVMRAC 的 iOS 開發者來講,已是一個上乘的例子,在不少地方都有說起。

咱們回顧一下:在這個程序裏,一個 ViewController(View層) 持有一個 ViewModel,一個 ViewModel 對應一個 Model。ViewController(View層) 對於 ViewModel 使用單向綁定,將 ViewModel 的變化反應到 ViewController(View層);ViewModel 對於 Model 使用雙向綁定,不論修改 ViewModel 或是 Model 都會實現數據的同步更新。

因而咱們把不少本來放在 ViewController 裏的邏輯獨立了出來,讓屬於 View層 的 ViewController 去作 View層 應該作的事情,而不要關心本來不屬於它的事情。固然咱們也沒有把獨立出來的這部分事情放在 Model 裏,並不污染真正屬於數據存儲部分的邏輯。因而其實咱們獨立出來的這個部分,就成了 ViewModel。

其餘參考文章

附註

@weakify(self); 宏實際上生成的代碼是:

@autoreleasepool {} __attribute__((objc_ownership(weak))) __typeof__(self) self_weak_ = (self);;

@strongify(self); 宏實際上生成的代碼是:

@autoreleasepool {} __attribute__((objc_ownership(strong))) __typeof__(self) self = self_weak_;
相關文章
相關標籤/搜索