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