iOS 關(guān)于MVVM With ReactiveCocoa設(shè)計模式的那些事

一、概述
  • 筆者 強烈推薦 大家在閱讀本文之前,還請先移步閱讀?? 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

    1. 系統(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ā)就會被子類覆蓋而中斷。
    1. 筆者在這里推薦可以使用Facebook開源的 KVOController,它比較優(yōu)雅地處理了 KVO 存在的一些問題,同時又能發(fā)揮KVO帶來的便捷性。優(yōu)雅的地方如下:
    • 不需要手動移除觀察者;
    • 實現(xiàn)KVO與事件發(fā)生處的代碼上下文相同,不需要跨方法傳參數(shù);
    • 使用block來替代方法能夠減少使用的復雜度,提升使用KVO的體驗;
    • 每一個keyPath會對應一個屬性,不需要在block中使用if - else判斷keyPath
  • 泛濫的狀態(tài)數(shù)監(jiān)聽
    上一篇 筆者通過分析(-(void)login)這個API的設(shè)計??,如果使用KVO的方式,那么視圖控制器就必須監(jiān)聽視圖模型executingerrorresponseObject的屬性變化,從而完成對視圖的處理。一個-(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來在viewviewModel之間充當 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)一的方法去處理異步的行為,包括 DelegateBlocks CallbacksTarget-Action機制NotificationsKVO
    它最大的與眾不同是提供了一種新的寫代碼的思維,由于RACCocoaKVOUIKit EventDelegateSelector等都增加了RAC支持,所以都不用去做很多跨函數(shù)的事,而且利用RAC處理事件很方便,可以把要處理的事情,和監(jiān)聽的事情的代碼放在一起,這樣非常方便我們管理,就不需要跳到對應的方法里。非常符合我們開發(fā)中高聚合,低耦合的思想。

  • ReactiveCocoa核心
    ReactiveCocoa核心就是RACSignalRACSignal (信號)對于 RAC 來說是構(gòu)造單元。它代表我們最終將要收到的信息,表示將來有數(shù)據(jù)傳遞,只要有數(shù)據(jù)改變,信號內(nèi)部接收到數(shù)據(jù),就會馬上發(fā)出數(shù)據(jù),所以你可以開始預先(陳述性)運用邏輯并構(gòu)建你的信息流,而不是必須等到事件發(fā)生(命令式)。
    信號會為了控制通過應用的信息流而獲得所有這些異步方法(委托回調(diào) block通知KVOtarget/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 的實踐思路。

  • 效果圖


  • 代碼實踐
    首先本文筆者著重講講登錄界面中viewModelview的部分關(guān)鍵代碼,探討一下MVVM的具體實踐過程。商品首頁界面的代碼實現(xiàn)的關(guān)鍵點還需要大家自行根據(jù)筆者提供Demo去體會,師傅領(lǐng)進門,修行靠個人
    登錄界面UI如下??:

    登錄界面效果圖二@2x.png

    登錄界面的主要元素如下:

    • 一個用于展示用戶頭像的圖片 userAvatar
    • 用于輸入賬號和密碼的輸入框phoneTextFieldverifyTextField
    • 一個用于登錄的按鈕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];
   }];
}

代碼梳理如下:

  • 觀察viewModelavatarUrlString屬性的變化,然后設(shè)置 userAvatar的圖片
  • viewModel中的mobilePhoneverifyCode屬性分別與phoneTextFieldverifyTextField輸入框中的內(nèi)容進行綁定;
  • loginButtonenabled屬性與viewModelvalidLoginSignal屬性進行綁定;
  • loginBtn按鈕被點擊時執(zhí)行loginCommand的命令;
  • 在填充(self.navigationItem.rightBarButtonItem)按鈕點擊時,賦值phoneTextFieldverifyTextFieldtext屬性的值。

綜上所述,我們將 SULoginController2 中的展示邏輯抽取到 SULoginViewModel2 中后,使得 SULoginController2 中的代碼更加簡潔和清晰。實踐MVVM的關(guān)鍵點在于,我們要能夠分析清楚 viewModel 需要暴露給view的數(shù)據(jù)和命令,這些數(shù)據(jù)和命令能夠代表view當前的狀態(tài)。換句話來說:使用MVC開發(fā)我們是 敲太多 ,而使用 MVVM 我們是 想太多

五、 填補細坑

使用RAC來實現(xiàn)ViewViewModel之間的數(shù)據(jù)綁定非常優(yōu)雅的同時也會使得Bug很難被調(diào)試。你看到界面異常了,有可能是你 View 的代碼有 Bug,也可能是 Model 的代碼有問題。數(shù)據(jù)綁定使得一個位置的 Bug 被快速傳遞到別的位置,要定位原始出問題的地方就變得不那么容易了。筆者通過使用RAC來實戰(zhàn)這個Demo也遇到了許多問題,特此分享出來,目的是少走一點彎路,填補一些細坑。

  1. 利用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)。

  1. 通過程序賦值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]];
  1. 一個對象同時綁定多個RACDynamicSignalCrash ,?? 請戳我
/// 登錄按鈕點擊
    /** 切記:如果按照下面??這樣寫會崩潰:原因是 一個對象只能綁定一個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];
     }];

解決辦法 ?? 請戳我

  1. 可變數(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,比如NSURLCachecurrentDiskUsage就不能被RACObserve。因為RAC是基于KVO的,NSMutableArray并不會在調(diào)用addObjectremoveObject時發(fā)送通知( willChangeValueForKey:didChangevlueForKey:),所以不可行。在使用RAC開發(fā)時,若要監(jiān)聽數(shù)組的變化,請將數(shù)組設(shè)計為不可變的數(shù)組(NSArray *dataSource),但是NSMutableArray也是可以添加KVO的 ?? 詳情請戳我

  1. 關(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做剩余的部分。
當然,RACUITableViewCell提供了一個方法:rac_prepareForReuseSignal,它的作用是當Cell即將要被重用時,告訴Cell。想象Cell上有多個buttonCell在初始化時給每個buttonaddTarget:action:forControlEvents,被重用時需要先移除這些target,下面這段代碼就可以很方便地解決這個問題:

[[[self.cancelButton
    rac_signalForControlEvents:UIControlEventTouchUpInside]
    takeUntil:self.rac_prepareForReuseSignal]
    subscribeNext:^(UIButton *x) {
    // do other things
}];
六、代碼閱讀

由于這個功能筆者分別采用 MVCMVVM Without ReactiveCococa以及MVVM Without ReactiveCococa來開發(fā)實踐,畢竟蘿卜白菜,各有所愛,目的就是便于大家更深層次的了解MVCMVVM的異同,以及提供一個利用MVVM真實開發(fā)的樣例,希望能夠打消大家對MVVM模式的顧慮。為了方便我們從宏觀上了解功能的的整體結(jié)構(gòu),我們可以分別看看MVCMVVM Without ReactiveCococa 以及MVVM WithReactiveCococa 的類圖。大家可以跟著類圖,順藤摸瓜,秉承該看的看,不該看的偷偷看的原則,趕快行動起來吧。

七、期待

文章若對您有點幫助,請給個喜歡??,畢竟碼字不易;若對您沒啥幫助,請給點建議??,切記學無止境。
針對文章所述內(nèi)容,閱讀期間任何疑問;請在文章底部批評指正,我會火速解決和修正問題。
GitHub地址:https://github.com/CoderMikeHe

八、參考鏈接
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容