C-41 是一個關於 MVVM
和 ReactiveCocoa
的開源程序,我是經過 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 的數據類型,其實就兩個 ASHRecipe
和 ASHStep
。框架
ASHMasterViewController
的 ViewModel 做爲 ASHMasterViewModel
的實例,繼承自 RVMViewModel
,這是一個第三方爲 RAC(ReactiveCocoa
)提供的 ViewModel 基類,可使用 CocoaPods 集成到項目裏。 RVMViewModel
假定一個 ViewModel 只對應一個 Model。mvvm
而後程序就進入 ASHMasterViewController
的控制範圍。佈局
ASHMasterViewController
和 ASHMasterViewModel
這個 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
獲取的。
ASHEditRecipeViewController
和 ASHEditRecipeViewModel
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,而是使用 ReactiveCocoa
對 textField
作的擴展 rac_textSignal
, 它其實是建立了一個監聽 textField
的 UIControlEventEditingChanged
事件的信號。 takeUntil:cell.rac_prepareForReuseSignal
則是指只有當 cell
的 -prepareForReuse
被調用時才觸發這個信號的 next
或 completed
事件。
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 的改變一般是須要雙向同步的。
ASHDetailViewController
和 ASHDetailViewModel
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
或 響應式編程
的文章中有所說起。
ASHTimerViewController
和 ASHTimerViewModel
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
特性來構建,對於剛剛接觸 MVVM
和 RAC
的 iOS 開發者來講,已是一個上乘的例子,在不少地方都有說起。
咱們回顧一下:在這個程序裏,一個 ViewController(View層) 持有一個 ViewModel,一個 ViewModel 對應一個 Model。ViewController(View層) 對於 ViewModel 使用單向綁定,將 ViewModel 的變化反應到 ViewController(View層);ViewModel 對於 Model 使用雙向綁定,不論修改 ViewModel 或是 Model 都會實現數據的同步更新。
因而咱們把不少本來放在 ViewController 裏的邏輯獨立了出來,讓屬於 View層 的 ViewController 去作 View層 應該作的事情,而不要關心本來不屬於它的事情。固然咱們也沒有把獨立出來的這部分事情放在 Model 裏,並不污染真正屬於數據存儲部分的邏輯。因而其實咱們獨立出來的這個部分,就成了 ViewModel。
MVVM
和 ReactiveCocoa
的文章翻譯(翻譯文章包含原文連接)
@weakify(self);
宏實際上生成的代碼是:
@autoreleasepool {} __attribute__((objc_ownership(weak))) __typeof__(self) self_weak_ = (self);;
@strongify(self);
宏實際上生成的代碼是:
@autoreleasepool {} __attribute__((objc_ownership(strong))) __typeof__(self) self = self_weak_;