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

一、概述
  • 筆者 強(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

    1. 系統(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)覆蓋而中斷。
    1. 筆者在這里推薦可以使用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à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)在viewviewModel之間充當(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)一的方法去處理異步的行為,包括 DelegateBlocks Callbacks,Target-Action機(jī)制,NotificationsKVO
    它最大的與眾不同是提供了一種新的寫(xiě)代碼的思維,由于RACCocoaKVOUIKit 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通知,KVOtarget/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í)踐
    首先本文筆者著重講講登錄界面中viewModelview的部分關(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)和密碼的輸入框phoneTextFieldverifyTextField;
    • 一個(gè)用于登錄的按鈕loginBtn
    • 一個(gè)用于的快速填充電話(huà)和驗(yàn)證碼的按鈕 fillupBtn。

分析:根據(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];
   }];
}

代碼梳理如下:

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

綜上所述,我們將 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)ViewViewModel之間的數(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ì)坑。

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

  1. 通過(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]];
  1. 一個(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)戳我

  1. 可變數(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,比如NSURLCachecurrentDiskUsage就不能被RACObserve。因?yàn)?code>RAC是基于KVO的,NSMutableArray并不會(huì)在調(diào)用addObjectremoveObject時(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)戳我

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

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

由于這個(gè)功能筆者分別采用 MVCMVVM Without ReactiveCococa以及MVVM Without ReactiveCococa來(lái)開(kāi)發(fā)實(shí)踐,畢竟蘿卜白菜,各有所愛(ài),目的就是便于大家更深層次的了解MVCMVVM的異同,以及提供一個(gè)利用MVVM真實(shí)開(kāi)發(fā)的樣例,希望能夠打消大家對(duì)MVVM模式的顧慮。為了方便我們從宏觀上了解功能的的整體結(jié)構(gòu),我們可以分別看看MVCMVVM 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

八、參考鏈接
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容