本文翻譯自MVVM Tutorial with ReactiveCocoa
- MVVM和數(shù)據(jù)綁定
MVVM模式依賴于數(shù)據(jù)綁定,能自動(dòng)將對(duì)象屬性和UI controls相聯(lián)系是其框架級(jí)的特性.
舉個(gè)例子,在微軟的WPF框架里,ViewModel將TextField里的Text屬性和Username屬性綁定,如下所示:
<TextField Text=”{DataBinding Path=Username, Mode=TwoWay}”/>
WPF框架將兩個(gè)屬性綁定在一起.
TwoWay綁定確保ViewModel中的Username屬性改變時(shí)會(huì)為T(mén)extField的Text屬性改變做準(zhǔn)備,而且可逆.例如用戶輸入時(shí)ViewModel的變化.
另一個(gè)例子是著名的基于MVVM的網(wǎng)頁(yè)框架Knockout,你可以在動(dòng)作里看到相似的綁定特性:
<input data-bind=”value: username”/>
上面將HTML元素的一個(gè)屬性和JavaScript模型綁定.
遺憾的是,iOS缺乏數(shù)據(jù)綁定的框架,但這正是ReactiveCocoa所扮演的角色:進(jìn)行ViewModel連接"粘合"工作.
從iOS開(kāi)發(fā)的角度來(lái)看MVVM模式,ViewController和其相關(guān)的UI(無(wú)論是nib、storyboard或者純代碼組成的View):

....通過(guò)ReactiveCocoa將它們綁定在一起.
理論知識(shí)已經(jīng)補(bǔ)的差不多了吧?如果不熟悉,可以回去復(fù)讀一下.如果覺(jué)得還可以,那就開(kāi)始編寫(xiě)ViewModels吧.
- 開(kāi)始項(xiàng)目架構(gòu)
首先下載一下初始項(xiàng)目:
項(xiàng)目用CocoaPods來(lái)管理依賴庫(kù)(如果你對(duì)CocoaPods不熟,這有你需要的教程].運(yùn)行命令行pod install來(lái)獲取依賴庫(kù),確保你會(huì)看到以下輸出:
$ pod install
Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.0.0)
Installing ReactiveCocoa (2.1.8)
Installing SDWebImage (3.6)
Installing objectiveflickr (2.0.4)
Generating Pods project
Integrating client project
你將會(huì)學(xué)到這些庫(kù)的很多用法.
初始項(xiàng)目已經(jīng)用view controllers和nib文件為你準(zhǔn)備好了應(yīng)用所需的視圖.打開(kāi)CocoaPods所生成的RWTFlickrSearch.xcworkspace,運(yùn)行后,你將看到其中的一個(gè)視圖:

花些時(shí)間來(lái)熟悉下項(xiàng)目的結(jié)構(gòu):

Model和ViewModel group是空的,待會(huì)你將在里面添加文件.View Group包含以下內(nèi)容:
- RWTFlickSearchViewController:程序的主界面,包含一個(gè)搜索text field和一個(gè)'Go'按鈕.
- RWTRecentSearchItemTableViewCell:在主界面展示最近搜索結(jié)果的table cell.
- RWTSearchResultsViewController:展示搜索結(jié)果Flickr圖片的table.
- RWTSearchResultsTableViewCell:展示單個(gè)Flickr圖片的table cell.
好了,該開(kāi)始編寫(xiě)你的第一個(gè)view model嚕.
- 你的首個(gè)ViewModel
在ViewModel組里添加一個(gè)名為RWTFlickrSearchModel的NSObject的子類(lèi).
打開(kāi)此文件的頭文件,添加如下屬性:
@interface RWTFlickrSearchViewModel : NSObject
@property (strong, nonatomic) NSString *searchText;
@property (strong, nonatomic) NSString *title;
@end
SearchText屬性為text field里面輸入的文字,title屬性為navigation bar上的title.
打開(kāi)RWTFlickrSearchViewModel.m添加如下代碼:
@implementation RWTFlickrSearchViewModel
- (instancetype)init {
self = [super init];
if (self) {
[self initialize];
}
return self;
}
- (void)initialize {
self.searchText = @"search text";
self.title = @"Flickr Search";
}
@end
以上代碼用來(lái)進(jìn)行ViewModel的初始化.
接下來(lái)將ViewModel和View連接.要記得View擁有對(duì)ViewModel的引用.當(dāng)前給出的是相應(yīng)ViewModel模型的View的初始化.
在RWTFlickrSearchViewController.h里導(dǎo)入ViewModel的頭文件:
#import "RWTFlickrSearchViewModel.h"
接著添加初始化方法:
@interface RWTFlickrSearchViewController : UIViewController
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel;
@end
在RWTFlickrSearchViewController.m里面添加一個(gè)私有屬性來(lái)控制UI outlets:
@property (weak, nonatomic) RWTFlickrSearchViewModel *viewModel;
繼而添加初始化方法:
- (instancetype)initWithViewModel:(RWTFlickrSearchViewModel *)viewModel {
self = [super init];
if (self) {
_viewModel = viewModel;
}
return self;
}
這將存儲(chǔ)一個(gè)View對(duì)ViewModel的引用.
在viewDidLoad的底部增加:
[self bindViewModel];
然后實(shí)現(xiàn)以下方法:
- (void)bindViewModel {
self.title = self.viewModel.title;
self.searchTextField.text = self.viewModel.searchText;
}
上面的代碼當(dāng)UI初始化和ViewModel狀態(tài)給View是調(diào)用.
最后是創(chuàng)建個(gè)ViewModel的實(shí)例以供View使用.
在RWTAppDelegate.m增加如下導(dǎo)入:
#import "RWTFlickrSearchViewModel.h"
添加一個(gè)私有屬性:
@property (strong, nonatomic) RWTFlickrSearchViewModel *viewModel;
你會(huì)發(fā)現(xiàn)這個(gè)類(lèi)已經(jīng)有個(gè)方法createInitialViewController,更新它的實(shí)現(xiàn)代碼:
- (UIViewController *)createInitialViewController {
self.viewModel = [RWTFlickrSearchViewModel new];
return [[RWTFlickrSearchViewController alloc] initWithViewModel:self.viewModel];
}
以上代碼用來(lái)創(chuàng)建一個(gè)ViewModel的新實(shí)例,繼而構(gòu)建和返回View.作用為初始化應(yīng)用的navigation controller.
運(yùn)行程序,你將看到View有一些狀態(tài)!

恭喜哦,這是你的第一個(gè)ViewModel.請(qǐng)克制你的興奮,還有許多要學(xué);]
也許你覺(jué)察到還木有用到ReactiveCocoa.當(dāng)前,如果你在搜索text field輸入內(nèi)容將不會(huì)傳遞到ViewModel.
- 檢測(cè)有效的搜索狀態(tài)
本章節(jié),你將使用ReactiveCocoa綁定ViewModel和View以使搜索text field和按鈕能和ViewModel連接.
在RWTFlickrSearchViewController.m更新bindViewModel;
- (void)bindViewModel {
self.title = self.viewModel.title;
RAC(self.viewModel, searchText) = self.searchTextField.rac_textSignal;
}
通過(guò)ReactiveCocoa的一個(gè)category給UITextField的類(lèi)添加了rac_textSignal屬性.這是個(gè)text field當(dāng)前文本內(nèi)容更新的信號(hào)量.
RAC宏是個(gè)綁定操作,上面的代碼通過(guò)rac_textSignal來(lái)監(jiān)測(cè)輸入的狀態(tài)實(shí)時(shí)更新viewModel中的searchText屬性.
簡(jiǎn)言之,就是確保seachText屬性能實(shí)時(shí)反映出當(dāng)前的UI狀態(tài).如果你覺(jué)得難以理解,那么最好去讀讀之前的兩篇ReactiveCocoa tutorials!
搜索按鈕只有在用戶輸入合法的文本時(shí)才能使用.我們?cè)O(shè)定只有在輸入超過(guò)三個(gè)字符時(shí)才能進(jìn)行搜索.
在RWTFlickrSearchViewModel.m里增加如下導(dǎo)入:
#import <ReactiveCocoa/ReactiveCocoa.h>
更新初始化方法里的內(nèi)容:
- (void)initialize {
self.title = @"Flickr Search";
RACSignal *validSearchSignal =
[[RACObserve(self, searchText)
map:^id(NSString *text) {
return @(text.length > 3);
}]
distinctUntilChanged];
[validSearchSignal subscribeNext:^(id x) {
NSLog(@"search text is valid %@", x);
}];
}
運(yùn)行程序,在text field里持續(xù)輸入內(nèi)容.你將從日志中看到text在合法和不合法狀態(tài)間改變:
2014-05-27 18:03:26.299 RWTFlickrSearch[13392:70b] search text is valid 0
2014-05-27 18:03:28.379 RWTFlickrSearch[13392:70b] search text is valid 1
2014-05-27 18:03:29.811 RWTFlickrSearch[13392:70b] search text is valid 0
上面的代碼使用RACObserve宏在ViewModel中的searchText屬性創(chuàng)建一個(gè)信號(hào)量(這就是ReactiveCocoa對(duì)KVO的包裝).一個(gè)map操作將text轉(zhuǎn)換為真假值.
最后distinctUnitlChanges使信號(hào)量只有在值狀態(tài)改變時(shí)發(fā)出.
截止現(xiàn)在你看到的是用ReactiveCocoa將View綁定到ViewModel,使之得到同步.另外,ReactiveCocoa經(jīng)常在ViewModel里來(lái)監(jiān)測(cè)它本身狀態(tài)來(lái)進(jìn)行其它操作.
這種類(lèi)型貫穿本MVVM教程.ReactiveCocoa用來(lái)綁定View和ViewModel,但在應(yīng)用其它layers里也會(huì)有用.
- 增加一個(gè)搜索指令
本章節(jié),你將使用validSearchSignal做更多事情:創(chuàng)建一個(gè)綁定View的指令.
在RWTFlickrSearchViewModel.h增加如下導(dǎo)入:
#import <ReactiveCocoa/ReactiveCocoa.h>
添加屬性:
@property (strong, nonatomic) RACCommand *executeSearch;
RACCommand是ReactiveCocoa中呈現(xiàn)UI動(dòng)作的組件.它包含一個(gè)來(lái)表示UI動(dòng)作結(jié)果、當(dāng)前狀態(tài)、標(biāo)明動(dòng)作是否被執(zhí)行的信號(hào)量.
在RWTFlickrSearchViewModel.m里的initialize方法的尾部增加:
self.executeSearch =
[[RACCommand alloc] initWithEnabled:validSearchSignal
signalBlock:^RACSignal *(id input) {
return [self executeSearchSignal];
}];
當(dāng)validSearchSignal為真時(shí)創(chuàng)建的指令為可用.
接著添加如下方法來(lái)提供創(chuàng)建執(zhí)行指令的信號(hào)量:
- (RACSignal *)executeSearchSignal {
return [[[[RACSignal empty]
logAll]
delay:2.0]
logAll];
}
在這個(gè)方法中將執(zhí)行一些業(yè)務(wù)邏輯作為執(zhí)行命令的結(jié)果,并會(huì)通過(guò)信號(hào)異步地返回結(jié)果.
目前只完成了一個(gè)虛擬的執(zhí)行情況;空信號(hào)立即完成.延遲操作增加了完成事件返回后的兩秒延遲.用來(lái)使代碼看起來(lái)更加真實(shí).
最后一步為將這個(gè)指令添加到View中.打開(kāi)RWTFlickrSearchViewController.m在bindViewModel方法的尾部添加:
self.searchButton.rac_command = self.viewModel.executeSearch;
上面的rac_command屬性為ReactiveCocoa給UIButton添加的擴(kuò)展.用來(lái)確保button點(diǎn)擊后指定的命令執(zhí)行,并且按鈕的可用狀態(tài)反應(yīng)了命令的可用狀態(tài).
運(yùn)行,輸入一些文本,點(diǎn)擊Go:

你會(huì)看到按鈕只有在輸入文本大于三個(gè)字符時(shí)才可用.而且當(dāng)年你點(diǎn)擊按鈕后兩秒內(nèi)不可用,當(dāng)執(zhí)行完信號(hào)量的時(shí)候才又變得可用了. 在console里,你將看到空信號(hào)量立即完成,兩秒后延遲操作執(zhí)行:
09:31:25.728 RWTFlickrSearch ... name: +empty completed
09:31:27.730 RWTFlickrSearch ... name: [+empty] -delay: 2.0 completed
是不是超酷?
Girl學(xué)iOS100天 第17天