一、概述
- 筆者 強(qiáng)烈推薦 大家在閱讀本文之前,還請(qǐng)先移步閱讀?? iOS 關(guān)于MVC和MVVM設(shè)計(jì)模式的那些事 和 ?? iOS 關(guān)于MVVM Without ReactiveCocoa設(shè)計(jì)模式的那些事 這兩篇文章,前者 詳細(xì)介紹了
MVC的基本知識(shí)和使用MVC將會(huì)給我們帶來(lái)哪些弊端,以及主要介紹MVVM的基本概念以及使用過(guò)程中哪些需要特別注意的基本原則。后者 主要是介紹MVVM各自的職責(zé)和他們之間的關(guān)系,以及在使用MVVM開(kāi)發(fā)的時(shí)候,視圖模型和子視圖模型各自使用的場(chǎng)景。 本文將會(huì)基于前兩篇文章來(lái)繼續(xù)探索:如何利用 ReactiveCocoa 更優(yōu)雅的實(shí)現(xiàn)MVVM。[注:后面統(tǒng)一用RAC代替ReactiveCocoa] - 通過(guò)上一篇文章的學(xué)習(xí),我們通過(guò)使用
MVVM Without ReactiveCocoa的方式成功將其運(yùn)用到實(shí)際項(xiàng)目中的開(kāi)發(fā),同時(shí)也讓我們明白:** MVVM的關(guān)鍵是要有ViewModel!而不是 ReactiveCocoa **。通過(guò)block以及KVO的方式照樣可以玩弄MVVM于股掌之間,但是這種方式的局限性想必有目共睹,其靈活性遠(yuǎn)遠(yuǎn)沒(méi)有使用ReactiveCocoa來(lái)的優(yōu)雅。本文將著重談?wù)?code>MVVM With ReactiveCocoa在iOS開(kāi)發(fā)中的實(shí)際運(yùn)用,以及自身通過(guò)實(shí)踐探索出來(lái)的經(jīng)驗(yàn)之談。但是關(guān)于ReactiveCocoa的具體使用還請(qǐng)自行Google和百度,本文可能更多的詮釋MVVM思想,而不是RAC的邏輯鏈?zhǔn)讲僮鳌?/li> - 本文只是筆者在實(shí)踐
MVVM過(guò)程中的些許見(jiàn)解,在此拋磚引玉,共同探討下MVVM的實(shí)踐思路,希望能夠打消你對(duì)MVVM模式的顧慮 ,提供一點(diǎn)思路,少走一些彎路,填補(bǔ)一些細(xì)坑。文章僅供大家參考,若有不妥之處,還望不吝賜教,歡迎批評(píng)指正。 -
MVVM基礎(chǔ)知識(shí)以及其使用注意不了解的,請(qǐng)務(wù)必戳我?? iOS 關(guān)于MVC和MVVM設(shè)計(jì)模式的那些事 - 使用
MVVM設(shè)計(jì)模式但是不打算使用ReactiveCocoa的,請(qǐng)務(wù)必戳我?? iOS 關(guān)于MVVM Without ReactiveCocoa設(shè)計(jì)模式的那些事
二、MVVM Without RAC的瑕疵
金無(wú)足赤,人無(wú)完人。雖然利用 MVVM + KVO這種方式,完全是可以很好的玩弄MVVM的,但是在使用過(guò)程中我們又不得吐槽它的瑕疵和局限性,這可能主要體現(xiàn)在以下幾個(gè)方面:
-
笨(操)重(蛋)的KVO
- 系統(tǒng)原生的
KVO操蛋的地方:
- 既需要進(jìn)行注冊(cè)成為某個(gè)對(duì)象屬性的觀察者,還需要手動(dòng)移除觀察者,且移除觀察者的時(shí)機(jī)必須合適 ; 同時(shí)你必須考慮父類(lèi)的
KVO事件觸發(fā)不被中斷,以及分別在父類(lèi)以及本類(lèi)中定義各自的context字符串以便在dealloc注銷(xiāo)的時(shí)候,區(qū)分移除的是本類(lèi)的kvo,還是父類(lèi)中的kvo,避免二次remove造成crash; - 注冊(cè)觀察者的代碼和事件發(fā)生處的代碼上下文不同,傳遞上下文是通過(guò)
void *指針; - 需要覆寫(xiě)又臭又長(zhǎng)的
-observeValueForKeyPath:ofObject:change:context:方法,比較麻煩; - 在復(fù)雜的業(yè)務(wù)邏輯中,準(zhǔn)確判斷被觀察者相對(duì)比較麻煩,有多個(gè)被觀測(cè)的對(duì)象和屬性時(shí),需要在方法中寫(xiě)大量的 if 進(jìn)行判斷;父類(lèi)的
KVO事件也需要考慮[super observeValueForKeyPath:keyPath ofObject:object change:change context:context],否則父類(lèi)的事件觸發(fā)就會(huì)被子類(lèi)覆蓋而中斷。
- 筆者在這里推薦可以使用
Facebook開(kāi)源的 KVOController,它比較優(yōu)雅地處理了KVO存在的一些問(wèn)題,同時(shí)又能發(fā)揮KVO帶來(lái)的便捷性。優(yōu)雅的地方如下:
- 不需要手動(dòng)移除觀察者;
- 實(shí)現(xiàn)
KVO與事件發(fā)生處的代碼上下文相同,不需要跨方法傳參數(shù); - 使用
block來(lái)替代方法能夠減少使用的復(fù)雜度,提升使用KVO的體驗(yàn); - 每一個(gè)
keyPath會(huì)對(duì)應(yīng)一個(gè)屬性,不需要在block中使用if - else判斷keyPath;
- 系統(tǒng)原生的
泛濫的狀態(tài)數(shù)監(jiān)聽(tīng)
上一篇 筆者通過(guò)分析(-(void)login)這個(gè)API的設(shè)計(jì)??,如果使用KVO的方式,那么視圖控制器就必須監(jiān)聽(tīng)視圖模型的executing、error、responseObject的屬性變化,從而完成對(duì)視圖的處理。一個(gè)-(void)login操作,就極其合理的在viewModel中衍生了三個(gè)狀態(tài),從而又衍生了viewController三個(gè)狀態(tài)監(jiān)聽(tīng)(KVO)。
/// 是否正在執(zhí)行
@property (nonatomic, readonly, assign) BOOL executing;
/// 請(qǐng)求失敗的信息
@property (nonatomic, readonly, strong) NSError *error;
/// 請(qǐng)求成功的數(shù)據(jù)
@property (nonatomic, readonly, strong) id responseObject;
/// 調(diào)起登錄
- (void) login;
要清楚MVVM中的 viewModel 仍然只是一個(gè)對(duì)象,主要是負(fù)責(zé)視圖的邏輯處理和數(shù)據(jù)轉(zhuǎn)換,而不是去維護(hù)一堆狀態(tài)(否則視圖模型將成為狀態(tài)數(shù)的重災(zāi)區(qū))。但我們?nèi)栽撆⒈M可能多的邏輯移到無(wú)狀態(tài)的函數(shù)值中,這樣我們將viewModel數(shù)據(jù)轉(zhuǎn)成給用戶(hù)在屏幕上看到的東西,避免了視圖控制器的復(fù)雜性。
-
多屬性變化處理事件的靈活性
實(shí)際開(kāi)發(fā)中,利用KVO只監(jiān)聽(tīng)某一個(gè)屬性的變化來(lái)處理業(yè)務(wù)邏輯,還是非常靈活的。但需要聯(lián)合多個(gè)屬性的變化來(lái)處理一些業(yè)務(wù)的時(shí)候,處理起來(lái)就會(huì)比較麻煩了。
三、ReactiveCocoa
綜上所述??,使用MVVM Without RAC開(kāi)發(fā)難免會(huì)存在一點(diǎn)瑕疵,ReactiveCocoa(RAC) 就是來(lái)拯救我們的。MVVM 在使用當(dāng)中,通常還會(huì)利用雙向綁定技術(shù),使得Model 變化時(shí),ViewModel會(huì)自動(dòng)更新,而ViewModel變化時(shí),View 也會(huì)自動(dòng)變化。MVVM開(kāi)發(fā)中可以使用RAC來(lái)在view和viewModel之間充當(dāng) binder的角色,優(yōu)雅地實(shí)現(xiàn)兩者之間數(shù)據(jù)同步,同時(shí)可以在viewModel中暴露RACSignal對(duì)象來(lái)替代像字符串和圖像這樣的屬性,這能在viewModel上消除更多的狀態(tài)以及一定程度上精簡(jiǎn)了ViewController上的代碼。
-
ReactiveCocoa簡(jiǎn)介
ReactiveCocoa(簡(jiǎn)稱(chēng)為RAC),是由Github開(kāi)源的一個(gè)應(yīng)用于iOS和OS開(kāi)發(fā)的新框架。RAC結(jié)合了函數(shù)式編程(Functional Programming)和響應(yīng)式編程(React Programming)的框架,也可稱(chēng)其為函數(shù)響應(yīng)式編程(FRP)框架 。
函數(shù)響應(yīng)式編程利用下圖??來(lái)解釋最好不過(guò)了:c = a + b定義好后,當(dāng)a的值變化后,c的值就會(huì)自動(dòng)變化。不過(guò)a的值變化時(shí)會(huì)產(chǎn)生一個(gè)信號(hào),這個(gè)信號(hào)會(huì)通知c根據(jù)a變化的值來(lái)變化自己的值。b的值變化同樣也影響c的值,這就是函數(shù)響應(yīng)式編程。
函數(shù)響應(yīng)式編程.png ReactiveCocoa作用
RAC最大的優(yōu)點(diǎn)是 提供了一個(gè)單一的、統(tǒng)一的方法去處理異步的行為,包括Delegate,Blocks Callbacks,Target-Action機(jī)制,Notifications和KVO。
它最大的與眾不同是提供了一種新的寫(xiě)代碼的思維,由于RAC將Cocoa中KVO、UIKit Event、Delegate、Selector等都增加了RAC支持,所以都不用去做很多跨函數(shù)的事,而且利用RAC處理事件很方便,可以把要處理的事情,和監(jiān)聽(tīng)的事情的代碼放在一起,這樣非常方便我們管理,就不需要跳到對(duì)應(yīng)的方法里。非常符合我們開(kāi)發(fā)中高聚合,低耦合的思想。-
ReactiveCocoa核心
ReactiveCocoa核心就是RACSignal。RACSignal (信號(hào))對(duì)于RAC來(lái)說(shuō)是構(gòu)造單元。它代表我們最終將要收到的信息,表示將來(lái)有數(shù)據(jù)傳遞,只要有數(shù)據(jù)改變,信號(hào)內(nèi)部接收到數(shù)據(jù),就會(huì)馬上發(fā)出數(shù)據(jù),所以你可以開(kāi)始預(yù)先(陳述性)運(yùn)用邏輯并構(gòu)建你的信息流,而不是必須等到事件發(fā)生(命令式)。
信號(hào)會(huì)為了控制通過(guò)應(yīng)用的信息流而獲得所有這些異步方法(委托,回調(diào) block,通知,KVO,target/action 事件觀察等)并將它們統(tǒng)一到一個(gè)接口下。不僅是這些,因?yàn)樾畔?huì)流過(guò)你的應(yīng)用, 它還提供給你輕松轉(zhuǎn)換/分解/合并/過(guò)濾信息的能力。
RACSignal的作用.png
默認(rèn)一個(gè)信號(hào)都是冷信號(hào),也就是值改變了,也不會(huì)觸發(fā);
/// 冷信號(hào)
RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
[subscriber sendNext:@"foobar"];
[subscriber sendCompleted];
return nil;
}];
只有訂閱了這個(gè)信號(hào),這個(gè)信號(hào)才會(huì)變?yōu)?code>熱信號(hào),值改變了才會(huì)觸發(fā)。
[signal subscribeCompleted:^{
NSLog(@"subscription %u", subscriptions);
}];
四、MVVM With RAC 代碼實(shí)踐
本文的實(shí)踐內(nèi)容與 上一篇 的需求一致,目的就是提供一個(gè)使用RAC來(lái)實(shí)現(xiàn)MVVM和不使用RAC來(lái)實(shí)現(xiàn)MVVM的異同以及各自的優(yōu)缺點(diǎn),更好為大家在現(xiàn)實(shí)開(kāi)發(fā)中是使用MVVM With RAC還是MVVM Without RAC提供一個(gè)不錯(cuò)的參考, 不了解的產(chǎn)品需求的讀者,請(qǐng)事先閱讀 上一篇 的UI設(shè)計(jì)和需求分析。這里就不在贅述了,還望見(jiàn)諒。這里筆者將會(huì)盡可能地回避具體的業(yè)務(wù)邏輯,重點(diǎn)關(guān)注MVVM With RAC 的實(shí)踐思路。
-
效果圖
-
代碼實(shí)踐
首先本文筆者著重講講登錄界面中viewModel和view的部分關(guān)鍵代碼,探討一下MVVM的具體實(shí)踐過(guò)程。商品首頁(yè)界面的代碼實(shí)現(xiàn)的關(guān)鍵點(diǎn)還需要大家自行根據(jù)筆者提供Demo去體會(huì),師傅領(lǐng)進(jìn)門(mén),修行靠個(gè)人。
登錄界面UI如下??:
登錄界面效果圖二@2x.png
登錄界面的主要元素如下:- 一個(gè)用于展示用戶(hù)頭像的圖片
userAvatar - 用于輸入賬號(hào)和密碼的輸入框
phoneTextField和verifyTextField; - 一個(gè)用于登錄的按鈕
loginBtn; - 一個(gè)用于的快速填充電話(huà)和驗(yàn)證碼的按鈕
fillupBtn。
- 一個(gè)用于展示用戶(hù)頭像的圖片
分析:根據(jù)我們前面對(duì)MVVM的探討,viewModel事先需要提供view所需的數(shù)據(jù)和命令。因此,SULoginViewModel2.h/m 頭文件的內(nèi)容大致如下:
/// 登錄界面的視圖模型
@interface SULoginViewModel2 : SUViewModel2
/// 手機(jī)號(hào)
@property (nonatomic, readwrite, copy) NSString *mobilePhone;
/// 驗(yàn)證碼
@property (nonatomic, readwrite, copy) NSString *verifyCode;
/// 用戶(hù)頭像
@property (nonatomic, readonly, copy) NSString *avatarUrlString;
/// 按鈕能否點(diǎn)擊
@property (nonatomic, readonly, strong) RACSignal *validLoginSignal;
/// 登錄按鈕點(diǎn)擊執(zhí)行的命令
@property (nonatomic, readonly, strong) RACCommand *loginCommand;
@end
- (void)initialize
{
[super initialize];
@weakify(self);
/// 數(shù)據(jù)綁定
RAC(self, avatarUrlString) = [[RACObserve(self, mobilePhone)
map:^NSString *(NSString * mobilePhone) {
/// 模擬從數(shù)據(jù)庫(kù)獲取用戶(hù)頭像的數(shù)據(jù)
/// 假數(shù)據(jù) 別在意
return ![NSString mh_isValidMobile:mobilePhone]?nil:[AppDelegate sharedDelegate].account.avatarUrl;
}]
distinctUntilChanged];
/// 按鈕有效性
self.validLoginSignal = [[RACSignal
combineLatest:@[ RACObserve(self, mobilePhone), RACObserve(self, verifyCode) ]
reduce:^(NSString *mobilePhone, NSString *verifyCode) {
return @(mobilePhone.length > 0 && verifyCode.length > 0);
}]
distinctUntilChanged];
/// 登錄命令
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
// 這里手機(jī)號(hào)以及驗(yàn)證碼在控制器那里也可以在視圖控制器篩選,但同時(shí)也可以在viewModel中處理
// 最好的寫(xiě)法:button.rac_command = viewmodel.loginCommand...把位數(shù)判斷移到這里
if (![NSString mh_isValidMobile:self.mobilePhone]) {
return [RACSignal error:[NSError errorWithDomain:SUCommandErrorDomain code:SUCommandErrorCode userInfo:@{SUCommandErrorUserInfoKey:@"請(qǐng)輸入正確的手機(jī)號(hào)碼"}]];
}
if (![NSString mh_isPureDigitCharacters:self.verifyCode] || self.verifyCode.length != 4 ) {
return [RACSignal error:[NSError errorWithDomain:SUCommandErrorDomain code:SUCommandErrorCode userInfo:@{SUCommandErrorUserInfoKey:@"驗(yàn)證碼錯(cuò)誤"}]];
}
@weakify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
@weakify(self);
/// 發(fā)起請(qǐng)求 模擬網(wǎng)絡(luò)請(qǐng)求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
/// 登錄成功 保存數(shù)據(jù) 簡(jiǎn)單起見(jiàn) 隨便存了哈
[[NSUserDefaults standardUserDefaults] setValue:self.mobilePhone forKey:SULoginPhoneKey2];
[[NSUserDefaults standardUserDefaults] setValue:self.verifyCode forKey:SULoginVerifyCodeKey2];
[[NSUserDefaults standardUserDefaults] synchronize];
/// 保存用戶(hù)數(shù)據(jù) 這個(gè)邏輯就不要我來(lái)實(shí)現(xiàn)了吧 假數(shù)據(jù)參照 [AppDelegate sharedDelegate].account
/// 模擬成功或者失敗
#if 1
[subscriber sendNext:nil];
/// 必須sendCompleted 否則command.executing一直為1 導(dǎo)致HUD 一直 loading
[subscriber sendCompleted];
#else
/// 失敗的回調(diào) 我就不處理 現(xiàn)實(shí)中開(kāi)發(fā)絕逼不是這樣的
[subscriber sendError:[NSError errorWithDomain:SUCommandErrorDomain code:SUCommandErrorCode userInfo:@{SUCommandErrorUserInfoKey:@"嗚嗚,服務(wù)器不給力呀..."}]];
#endif
});
return nil;
}];
}];
}
代碼梳理如下:
-
.h中的validLoginSignal屬性代表的是登錄按鈕是否可用,它將會(huì)與view中登錄按鈕的enabled屬性進(jìn)行綁定。 - 當(dāng)用戶(hù)輸入手機(jī)號(hào)碼時(shí),調(diào)用
model層的方法查詢(xún)本地?cái)?shù)據(jù)庫(kù)中緩存的用戶(hù)數(shù)據(jù),并返回avatarUrlString屬性; - 當(dāng)用戶(hù)輸入的手機(jī)號(hào)碼或驗(yàn)證碼發(fā)生變化時(shí),判斷手機(jī)號(hào)碼和密碼的長(zhǎng)度是否均大于 0 ,如果是則登錄按鈕可用,否則不可用;
- 當(dāng)
loginCommand命令執(zhí)行成功時(shí),處理自己的業(yè)務(wù)邏輯。
接下來(lái)看看,SULoginController2中的關(guān)鍵代碼:
- (void)bindViewModel
{
[super bindViewModel];
@weakify(self);
/// 判定數(shù)據(jù)
[RACObserve(self.viewModel, avatarUrlString) subscribeNext:^(NSString *avatarUrlString) {
@strongify(self);
[MHWebImageTool setImageWithURL:avatarUrlString placeholderImage:placeholderUserIcon() imageView:self.userAvatar];
}];
RAC(self.viewModel , mobilePhone) = [RACSignal merge:@[RACObserve(self.inputView.phoneTextField, text),self.inputView.phoneTextField.rac_textSignal]];
RAC(self.viewModel , verifyCode) = [RACSignal merge:@[RACObserve(self.inputView.verifyTextField, text),self.inputView.verifyTextField.rac_textSignal]];
RAC(self.loginBtn , enabled) = self.viewModel.validLoginSignal;
[[[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x) {
@strongify(self);
[self.view endEditing:YES];
[MBProgressHUD mh_showProgressHUD:@"Loading..."];
}]
subscribeNext:^(UIButton *sender) {
@strongify(self);
[self.viewModel.loginCommand execute:nil];
}];
/// 數(shù)據(jù)成功
[self.viewModel.loginCommand.executionSignals.switchToLatest
subscribeNext:^(id x) {
@strongify(self);
[MBProgressHUD mh_hideHUD];
/// 跳轉(zhuǎn)
SUGoodsViewModel2 *viewModel = [[SUGoodsViewModel2 alloc] initWithParams:@{}];
SUGoodsController2 *goodsVc = [[SUGoodsController2 alloc] initWithViewModel:viewModel];
[self.navigationController pushViewController:goodsVc animated:YES];
}];
/// 錯(cuò)誤信息
[self.viewModel.loginCommand.errors subscribeNext:^(NSError *error) {
/// 處理驗(yàn)證錯(cuò)誤的error
if ([error.domain isEqualToString:SUCommandErrorDomain]) {
[MBProgressHUD mh_showTips:error.userInfo[SUCommandErrorUserInfoKey]];
return ;
}
[MBProgressHUD mh_showErrorTips:error];
}];
}
代碼梳理如下:
- 觀察
viewModel中avatarUrlString屬性的變化,然后設(shè)置userAvatar的圖片 - 將
viewModel中的mobilePhone和verifyCode屬性分別與phoneTextField和verifyTextField輸入框中的內(nèi)容進(jìn)行綁定; - 將
loginButton的enabled屬性與viewModel的validLoginSignal屬性進(jìn)行綁定; - 在
loginBtn按鈕被點(diǎn)擊時(shí)執(zhí)行loginCommand的命令; - 在填充(self.navigationItem.rightBarButtonItem)按鈕點(diǎn)擊時(shí),賦值
phoneTextField和verifyTextField的text屬性的值。
綜上所述,我們將 SULoginController2 中的展示邏輯抽取到 SULoginViewModel2 中后,使得 SULoginController2 中的代碼更加簡(jiǎn)潔和清晰。實(shí)踐MVVM的關(guān)鍵點(diǎn)在于,我們要能夠分析清楚 viewModel 需要暴露給view的數(shù)據(jù)和命令,這些數(shù)據(jù)和命令能夠代表view當(dāng)前的狀態(tài)。換句話(huà)來(lái)說(shuō):使用MVC開(kāi)發(fā)我們是 敲太多 ,而使用 MVVM 我們是 想太多。
五、 填補(bǔ)細(xì)坑
使用RAC來(lái)實(shí)現(xiàn)View和ViewModel之間的數(shù)據(jù)綁定非常優(yōu)雅的同時(shí)也會(huì)使得Bug很難被調(diào)試。你看到界面異常了,有可能是你 View 的代碼有 Bug,也可能是 Model 的代碼有問(wèn)題。數(shù)據(jù)綁定使得一個(gè)位置的 Bug 被快速傳遞到別的位置,要定位原始出問(wèn)題的地方就變得不那么容易了。筆者通過(guò)使用RAC來(lái)實(shí)戰(zhàn)這個(gè)Demo也遇到了許多問(wèn)題,特此分享出來(lái),目的是少走一點(diǎn)彎路,填補(bǔ)一些細(xì)坑。
- 利用
RACCommand來(lái)處理網(wǎng)絡(luò)請(qǐng)求的坑
/// 登錄命令
self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
@weakify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
@weakify(self);
/// 發(fā)起請(qǐng)求 模擬網(wǎng)絡(luò)請(qǐng)求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
@strongify(self);
/// 登錄成功 保存數(shù)據(jù) 簡(jiǎn)單起見(jiàn) 隨便存了哈
[[NSUserDefaults standardUserDefaults] setValue:self.mobilePhone forKey:SULoginPhoneKey2];
[[NSUserDefaults standardUserDefaults] setValue:self.verifyCode forKey:SULoginVerifyCodeKey2];
[[NSUserDefaults standardUserDefaults] synchronize];
/// 保存用戶(hù)數(shù)據(jù) 這個(gè)邏輯就不要我來(lái)實(shí)現(xiàn)了吧 假數(shù)據(jù)參照 [AppDelegate sharedDelegate].account
[subscriber sendNext:nil];
[subscriber sendCompleted];
});
return nil;
}];
}];
切記在實(shí)踐過(guò)程中,如果成功請(qǐng)求到網(wǎng)絡(luò)數(shù)據(jù),調(diào)用[subscriber sendNext:nil];的同時(shí)必須調(diào)用[subscriber sendCompleted],這樣才能保證命令已經(jīng)執(zhí)行完畢。否則 command.executing 一直傳遞的是1,從而導(dǎo)致HUD一直處在 loading 的狀態(tài)。
- 通過(guò)程序賦值
phoneTextField.text = @"xxx",不會(huì)觸發(fā)phoneTextField.rac_textSignal的事件的坑?? 請(qǐng)戳我
/***
/// Fixed:rac_textSignal只有用戶(hù)輸入才有效,如果只是直接賦值 eg:self.inputView.phoneTextField.text = @"xxxx" 這樣self.inputView.phoneTextField.rac_textSignal就不會(huì)觸發(fā)的。
/// 解決辦法:利用 RACObserve 來(lái)觀察self.inputView.phoneTextField.text的賦值辦法即可
/// 用戶(hù)輸入的情況 觸發(fā)rac_textSignal
/// 用戶(hù)非輸入而是直接賦值的情況 觸發(fā)RACObserve
RAC(self.viewModel , mobilePhone) = self.inputView.phoneTextField.rac_textSignal;
RAC(self.viewModel , verifyCode) = self.inputView.verifyTextField.rac_textSignal;
**/
RAC(self.viewModel , mobilePhone) = [RACSignal merge:@[RACObserve(self.inputView.phoneTextField, text),self.inputView.phoneTextField.rac_textSignal]];
RAC(self.viewModel , verifyCode) = [RACSignal merge:@[RACObserve(self.inputView.verifyTextField, text),self.inputView.verifyTextField.rac_textSignal]];
- 一個(gè)對(duì)象同時(shí)綁定多個(gè)
RACDynamicSignal會(huì)Crash,?? 請(qǐng)戳我
/// 登錄按鈕點(diǎn)擊
/** 切記:如果按照下面??這樣寫(xiě)會(huì)崩潰:原因是 一個(gè)對(duì)象只能綁定一個(gè)RACDynamicSignal的信號(hào)
RAC(self.loginBtn , enabled) = self.viewModel.validLoginSignal;
self.loginBtn.rac_command = self.viewModel.loginCommand;
reason:'Signal <RACDynamicSignal: 0x60800023d3e0> name: is already bound to key path "enabled" on object <UIButton: 0x7f8448c57690; frame = (12 362; 351 49); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x60800023dae0>>, adding signal <RACReplaySubject: 0x60000027ce00> name: is undefined behavior'
*/
/// ??為正確的打開(kāi)方式
RAC(self.loginBtn , enabled) = self.viewModel.validLoginSignal;
[[[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x) {
@strongify(self);
[self.view endEditing:YES];
[MBProgressHUD mh_showProgressHUD:@"Loading..."];
}]
subscribeNext:^(UIButton *sender) {
@strongify(self);
[self.viewModel.loginCommand execute:nil];
}];
解決辦法 ?? 請(qǐng)戳我
- 可變數(shù)組(字典/...)不能被
RACObserve?? 請(qǐng)戳我
/// The data source of table view. 這里不能用NSMutableArray,因?yàn)镹SMutableArray不支持KVO,不能被RACObserve
@property (nonatomic, readwrite, copy) NSArray *dataSource;
注意:RACObserve使用了KVO來(lái)監(jiān)聽(tīng)property的變化,只要property被自己或外部改變,block就會(huì)被執(zhí)行。但不是所有的property都可以被RACObserve,該property必須支持KVO,比如NSURLCache的currentDiskUsage就不能被RACObserve。因?yàn)?code>RAC是基于KVO的,NSMutableArray并不會(huì)在調(diào)用addObject或removeObject時(shí)發(fā)送通知( willChangeValueForKey:和didChangevlueForKey:),所以不可行。在使用RAC開(kāi)發(fā)時(shí),若要監(jiān)聽(tīng)數(shù)組的變化,請(qǐng)將數(shù)組設(shè)計(jì)為不可變的數(shù)組(NSArray *dataSource),但是NSMutableArray也是可以添加KVO的 ?? 詳情請(qǐng)戳我 。
- 關(guān)于
Cell復(fù)用時(shí)清理數(shù)據(jù)綁定或者事件監(jiān)聽(tīng)的問(wèn)題
@implementation SUGoodsCell
- (void) awakeFromNib {
[super awakeFromNib];
RAC(self.usernameLabel, text) = RACObserve(self, viewModel. username);
RAC(self.userIdLabel, text) = RACObserve(self, viewModel. userId);
}
注意viewModel出現(xiàn)在RACObserve宏中逗號(hào)右邊。 這些 cell 終將被重用,新的viewModels將會(huì)被賦值,如果我們不將 viewModel放在逗號(hào)右邊,那就會(huì)監(jiān)聽(tīng)viewModel屬性的變化然后每次都要重新設(shè)置綁定;如果放在逗號(hào)右邊, RACObserve 將會(huì)為我們負(fù)責(zé)這些事兒, 因此我們只需要設(shè)定一次綁定并讓Reactive Cocoa做剩余的部分。
當(dāng)然,RAC給UITableViewCell提供了一個(gè)方法:rac_prepareForReuseSignal,它的作用是當(dāng)Cell即將要被重用時(shí),告訴Cell。想象Cell上有多個(gè)button,Cell在初始化時(shí)給每個(gè)button都addTarget:action:forControlEvents,被重用時(shí)需要先移除這些target,下面這段代碼就可以很方便地解決這個(gè)問(wèn)題:
[[[self.cancelButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
takeUntil:self.rac_prepareForReuseSignal]
subscribeNext:^(UIButton *x) {
// do other things
}];
六、代碼閱讀
由于這個(gè)功能筆者分別采用 MVC和MVVM Without ReactiveCococa以及MVVM Without ReactiveCococa來(lái)開(kāi)發(fā)實(shí)踐,畢竟蘿卜白菜,各有所愛(ài),目的就是便于大家更深層次的了解MVC和MVVM的異同,以及提供一個(gè)利用MVVM真實(shí)開(kāi)發(fā)的樣例,希望能夠打消大家對(duì)MVVM模式的顧慮。為了方便我們從宏觀上了解功能的的整體結(jié)構(gòu),我們可以分別看看MVC和MVVM Without ReactiveCococa 以及MVVM WithReactiveCococa 的類(lèi)圖。大家可以跟著類(lèi)圖,順藤摸瓜,秉承該看的看,不該看的偷偷看的原則,趕快行動(dòng)起來(lái)吧。
-
MVC 的類(lèi)圖
MVC類(lèi)圖.png -
MVVM Without ReactiveCococa 的類(lèi)圖
MVVMWithoutRAC類(lèi)圖.png -
MVVM With ReactiveCococa 的類(lèi)圖
MVVMWithRAC類(lèi)圖.png 源碼地址(PS: 還請(qǐng)star一下,不會(huì)懷孕??的)MHDevelopExample_Objective_C 目錄中的 MVC&MVVM文件夾中
七、期待
文章若對(duì)您有點(diǎn)幫助,請(qǐng)給個(gè)喜歡??,畢竟碼字不易;若對(duì)您沒(méi)啥幫助,請(qǐng)給點(diǎn)建議??,切記學(xué)無(wú)止境。
針對(duì)文章所述內(nèi)容,閱讀期間任何疑問(wèn);請(qǐng)?jiān)谖恼碌撞颗u(píng)指正,我會(huì)火速解決和修正問(wèn)題。
GitHub地址:https://github.com/CoderMikeHe
八、參考鏈接
- http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/ ?? 譯文
- http://blog.leichunfeng.com/blog/2016/02/27/mvvm-with-reactivecocoa/
- https://github.com/leichunfeng/MVVMReactiveCocoa
- http://draveness.me/kvocontroller.html
- http://www.cnblogs.com/wengzilin/p/4346775.html
- http://www.cocoachina.com/ios/20151020/13795.html






