iOS 关于MVVM With ReactiveCocoa设计模

作者: CoderMikeHe | 来源:发表于2017-06-26 13:40 被阅读4264次
    一、概述
    • 笔者 强烈推荐 大家在阅读本文之前,还请先移步阅读👉 iOS 关于MVC和MVVM设计模式的那些事 和 👉 iOS 关于MVVM Without ReactiveCocoa设计模式的那些事 这两篇文章,前者 详细介绍了MVC的基本知识和使用MVC将会给我们带来哪些弊端,以及主要介绍MVVM的基本概念以及使用过程中哪些需要特别注意的基本原则。后者 主要是介绍MVVM各自的职责和他们之间的关系,以及在使用MVVM开发的时候,视图模型和子视图模型各自使用的场景。 本文将会基于前两篇文章来继续探索:如何利用 ReactiveCocoa 更优雅的实现MVVM。[注:后面统一用 RAC 代替 ReactiveCocoa]
    • 通过上一篇文章的学习,我们通过使用MVVM Without ReactiveCocoa的方式成功将其运用到实际项目中的开发,同时也让我们明白:** MVVM的关键是要有ViewModel!而不是 ReactiveCocoa **。通过block以及KVO的方式照样可以玩弄MVVM于股掌之间,但是这种方式的局限性想必有目共睹,其灵活性远远没有使用ReactiveCocoa来的优雅。本文将着重谈谈MVVM With ReactiveCocoa在iOS开发中的实际运用,以及自身通过实践探索出来的经验之谈。但是关于ReactiveCocoa的具体使用还请自行Google百度,本文可能更多的诠释MVVM思想,而不是RAC的逻辑链式操作。
    • 本文只是笔者在实践MVVM过程中的些许见解,在此抛砖引玉,共同探讨下 MVVM 的实践思路,希望能够打消你对 MVVM 模式的顾虑 ,提供一点思路,少走一些弯路,填补一些细坑。文章仅供大家参考,若有不妥之处,还望不吝赐教,欢迎批评指正。
    • MVVM基础知识以及其使用注意不了解的,请务必戳我👉 iOS 关于MVC和MVVM设计模式的那些事
    • 使用MVVM设计模式但是不打算使用ReactiveCocoa的,请务必戳我👉 iOS 关于MVVM Without ReactiveCocoa设计模式的那些事
    二、MVVM Without RAC的瑕疵

    金无足赤,人无完人。虽然利用 MVVM + KVO这种方式,完全是可以很好的玩弄MVVM的,但是在使用过程中我们又不得吐槽它的瑕疵和局限性,这可能主要体现在以下几个方面:

    • 笨(操)重(蛋)的KVO

      1. 系统原生的KVO操蛋的地方:
      • 既需要进行注册成为某个对象属性的观察者,还需要手动移除观察者,且移除观察者的时机必须合适 ; 同时你必须考虑父类的KVO事件触发不被中断,以及分别在父类以及本类中定义各自的context字符串以便在dealloc注销的时候,区分移除的是本类的kvo,还是父类中的kvo,避免二次remove造成crash
      • 注册观察者的代码和事件发生处的代码上下文不同,传递上下文是通过void *指针;
      • 需要覆写又臭又长的-observeValueForKeyPath:ofObject:change:context:方法,比较麻烦;
      • 在复杂的业务逻辑中,准确判断被观察者相对比较麻烦,有多个被观测的对象和属性时,需要在方法中写大量的 if 进行判断;父类的KVO事件也需要考虑 [super observeValueForKeyPath:keyPath ofObject:object change:change context:context],否则父类的事件触发就会被子类覆盖而中断。
      1. 笔者在这里推荐可以使用Facebook开源的 KVOController,它比较优雅地处理了 KVO 存在的一些问题,同时又能发挥KVO带来的便捷性。优雅的地方如下:
      • 不需要手动移除观察者;
      • 实现KVO与事件发生处的代码上下文相同,不需要跨方法传参数;
      • 使用block来替代方法能够减少使用的复杂度,提升使用KVO的体验;
      • 每一个keyPath会对应一个属性,不需要在block中使用if - else判断keyPath
    • 泛滥的状态数监听
      上一篇 笔者通过分析(-(void)login)这个API的设计👇,如果使用KVO的方式,那么视图控制器就必须监听视图模型executingerrorresponseObject的属性变化,从而完成对视图的处理。一个-(void)login操作,就极其合理的在viewModel中衍生了三个状态,从而又衍生了viewController三个状态监听(KVO)。

     /// 是否正在执行
     @property (nonatomic, readonly, assign) BOOL executing;
     /// 请求失败的信息
     @property (nonatomic, readonly, strong) NSError *error;
     /// 请求成功的数据
     @property (nonatomic, readonly, strong) id responseObject;
     /// 调起登录
     - (void) login;
    

    要清楚MVVM中的 viewModel 仍然只是一个对象,主要是负责视图的逻辑处理和数据转换,而不是去维护一堆状态(否则视图模型将成为状态数的重灾区)。但我们仍该努力将尽可能多的逻辑移到无状态的函数值中,这样我们将viewModel数据转成给用户在屏幕上看到的东西,避免了视图控制器的复杂性。

    • 多属性变化处理事件的灵活性
      实际开发中,利用KVO只监听某一个属性的变化来处理业务逻辑,还是非常灵活的。但需要联合多个属性的变化来处理一些业务的时候,处理起来就会比较麻烦了。
    三、ReactiveCocoa

    综上所述👆,使用MVVM Without RAC开发难免会存在一点瑕疵,ReactiveCocoa(RAC) 就是来拯救我们的。MVVM 在使用当中,通常还会利用双向绑定技术,使得Model 变化时,ViewModel会自动更新,而ViewModel变化时,View 也会自动变化。MVVM开发中可以使用RAC来在viewviewModel之间充当 binder的角色,优雅地实现两者之间数据同步,同时可以在viewModel中暴露RACSignal对象来替代像字符串和图像这样的属性,这能在viewModel上消除更多的状态以及一定程度上精简了ViewController上的代码。

    • ReactiveCocoa简介
      ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架。RAC结合了函数式编程(Functional Programming)响应式编程(React Programming)的框架,也可称其为函数响应式编程(FRP)框架 。
      函数响应式编程利用下图👇来解释最好不过了:c = a + b 定义好后,当a的值变化后,c的值就会自动变化。不过a的值变化时会产生一个信号,这个信号会通知c根据a变化的值来变化自己的值。b的值变化同样也影响c的值,这就是函数响应式编程。

      函数响应式编程.png
    • ReactiveCocoa作用
      RAC最大的优点是 提供了一个单一的、统一的方法去处理异步的行为,包括 DelegateBlocks CallbacksTarget-Action机制NotificationsKVO
      它最大的与众不同是提供了一种新的写代码的思维,由于RACCocoaKVOUIKit EventDelegateSelector等都增加了RAC支持,所以都不用去做很多跨函数的事,而且利用RAC处理事件很方便,可以把要处理的事情,和监听的事情的代码放在一起,这样非常方便我们管理,就不需要跳到对应的方法里。非常符合我们开发中高聚合,低耦合的思想。

    • ReactiveCocoa核心
      ReactiveCocoa核心就是RACSignalRACSignal (信号)对于 RAC 来说是构造单元。它代表我们最终将要收到的信息,表示将来有数据传递,只要有数据改变,信号内部接收到数据,就会马上发出数据,所以你可以开始预先(陈述性)运用逻辑并构建你的信息流,而不是必须等到事件发生(命令式)。
      信号会为了控制通过应用的信息流而获得所有这些异步方法(委托回调 block通知KVOtarget/action 事件观察等)并将它们统一到一个接口下。不仅是这些,因为信息会流过你的应用, 它还提供给你轻松转换/分解/合并/过滤信息的能力。

      RACSignal的作用.png
      默认一个信号都是冷信号,也就是值改变了,也不会触发;
    /// 冷信号
    RACSignal *signal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) { 
        [subscriber sendNext:@"foobar"]; 
        [subscriber sendCompleted]; 
        return nil; 
    }]; 
    

    只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。

    [signal subscribeCompleted:^{ 
        NSLog(@"subscription %u", subscriptions); 
    }]; 
    
    四、MVVM With RAC 代码实践

    本文的实践内容与 上一篇 的需求一致,目的就是提供一个使用RAC来实现MVVM不使用RAC来实现MVVM的异同以及各自的优缺点,更好为大家在现实开发中是使用MVVM With RAC还是MVVM Without RAC提供一个不错的参考, 不了解的产品需求的读者,请事先阅读 上一篇 的UI设计和需求分析。这里就不在赘述了,还望见谅。这里笔者将会尽可能地回避具体的业务逻辑,重点关注MVVM With RAC 的实践思路。

    • 效果图


    • 代码实践
      首先本文笔者着重讲讲登录界面中viewModelview的部分关键代码,探讨一下MVVM的具体实践过程。商品首页界面的代码实现的关键点还需要大家自行根据笔者提供Demo去体会,师傅领进门,修行靠个人
      登录界面UI如下👇:

      登录界面效果图二@2x.png
      登录界面的主要元素如下:
      • 一个用于展示用户头像的图片 userAvatar
      • 用于输入账号和密码的输入框phoneTextFieldverifyTextField
      • 一个用于登录的按钮loginBtn
      • 一个用于的快速填充电话和验证码的按钮 fillupBtn

    分析:根据我们前面对MVVM的探讨,viewModel事先需要提供view所需的数据和命令。因此,SULoginViewModel2.h/m 头文件的内容大致如下:

    /// 登录界面的视图模型
    @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;
    /// 登录按钮点击执行的命令
    @property (nonatomic, readonly, strong) RACCommand *loginCommand;
     @end
    
     - (void)initialize
    {
        [super initialize];
        @weakify(self);
        
        /// 数据绑定
        RAC(self, avatarUrlString) = [[RACObserve(self, mobilePhone)
                                 map:^NSString *(NSString * mobilePhone) {
                                    /// 模拟从数据库获取用户头像的数据
                                    /// 假数据 别在意
                                    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...把位数判断移到这里
            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);
                /// 发起请求 模拟网络请求
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    @strongify(self);
                    /// 登录成功 保存数据 简单起见 随便存了哈
                    [[NSUserDefaults standardUserDefaults] setValue:self.mobilePhone forKey:SULoginPhoneKey2];
                    [[NSUserDefaults standardUserDefaults] setValue:self.verifyCode forKey:SULoginVerifyCodeKey2];
                    [[NSUserDefaults standardUserDefaults] synchronize];
                    /// 保存用户数据 这个逻辑就不要我来实现了吧 假数据参照 [AppDelegate sharedDelegate].account
                    /// 模拟成功或者失败
    #if 1
                    [subscriber sendNext:nil];
                    /// 必须sendCompleted 否则command.executing一直为1 导致HUD 一直 loading
                    [subscriber sendCompleted];
    #else
                    /// 失败的回调 我就不处理 现实中开发绝逼不是这样的
                    [subscriber sendError:[NSError errorWithDomain:SUCommandErrorDomain code:SUCommandErrorCode userInfo:@{SUCommandErrorUserInfoKey:@"呜呜,服务器不给力呀..."}]];
    #endif
                });
                
                return nil;
            }];
        }];
    }
    

    代码梳理如下:

    • .h中的validLoginSignal属性代表的是登录按钮是否可用,它将会与view中登录按钮的enabled属性进行绑定。
    • 当用户输入手机号码时,调用model层的方法查询本地数据库中缓存的用户数据,并返回avatarUrlString属性;
    • 当用户输入的手机号码或验证码发生变化时,判断手机号码和密码的长度是否均大于 0 ,如果是则登录按钮可用,否则不可用;
    • loginCommand命令执行成功时,处理自己的业务逻辑。

    接下来看看,SULoginController2中的关键代码:

    - (void)bindViewModel
    {   
       [super bindViewModel];
       
       @weakify(self);
       
       /// 判定数据
       [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];
        }];
    
       /// 数据成功
       [self.viewModel.loginCommand.executionSignals.switchToLatest
        subscribeNext:^(id x) {
            @strongify(self);
            [MBProgressHUD mh_hideHUD];
            /// 跳转
            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属性的变化,然后设置 userAvatar的图片
    • viewModel中的mobilePhoneverifyCode属性分别与phoneTextFieldverifyTextField输入框中的内容进行绑定;
    • loginButtonenabled属性与viewModelvalidLoginSignal属性进行绑定;
    • loginBtn按钮被点击时执行loginCommand的命令;
    • 在填充(self.navigationItem.rightBarButtonItem)按钮点击时,赋值phoneTextFieldverifyTextFieldtext属性的值。

    综上所述,我们将 SULoginController2 中的展示逻辑抽取到 SULoginViewModel2 中后,使得 SULoginController2 中的代码更加简洁和清晰。实践MVVM的关键点在于,我们要能够分析清楚 viewModel 需要暴露给view的数据和命令,这些数据和命令能够代表view当前的状态。换句话来说:使用MVC开发我们是 敲太多 ,而使用 MVVM 我们是 想太多

    五、 填补细坑

    使用RAC来实现ViewViewModel之间的数据绑定非常优雅的同时也会使得Bug很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。笔者通过使用RAC来实战这个Demo也遇到了许多问题,特此分享出来,目的是少走一点弯路,填补一些细坑。

    1. 利用RACCommand来处理网络请求的坑
        /// 登录命令
        self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            @strongify(self);
            @weakify(self);
            return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                @strongify(self);
                @weakify(self);
                /// 发起请求 模拟网络请求
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    @strongify(self);
                    /// 登录成功 保存数据 简单起见 随便存了哈
                    [[NSUserDefaults standardUserDefaults] setValue:self.mobilePhone forKey:SULoginPhoneKey2];
                    [[NSUserDefaults standardUserDefaults] setValue:self.verifyCode forKey:SULoginVerifyCodeKey2];
                    [[NSUserDefaults standardUserDefaults] synchronize];
                    /// 保存用户数据 这个逻辑就不要我来实现了吧 假数据参照 [AppDelegate sharedDelegate].account
                    [subscriber sendNext:nil];
                    [subscriber sendCompleted];
                });
                return nil;
            }];
        }];
    

    切记在实践过程中,如果成功请求到网络数据,调用[subscriber sendNext:nil];的同时必须调用[subscriber sendCompleted],这样才能保证命令已经执行完毕。否则 command.executing 一直传递的是1,从而导致HUD一直处在 loading 的状态。

    1. 通过程序赋值phoneTextField.text = @"xxx",不会触发phoneTextField.rac_textSignal的事件的坑👉 请戳我
    /***
        /// Fixed:rac_textSignal只有用户输入才有效,如果只是直接赋值 eg:self.inputView.phoneTextField.text = @"xxxx"  这样self.inputView.phoneTextField.rac_textSignal就不会触发的。
        /// 解决办法:利用 RACObserve 来观察self.inputView.phoneTextField.text的赋值办法即可
        /// 用户输入的情况 触发rac_textSignal
        /// 用户非输入而是直接赋值的情况 触发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. 可变数组(字典/...)不能被RACObserve 👉 请戳我
    /// The data source of table view. 这里不能用NSMutableArray,因为NSMutableArray不支持KVO,不能被RACObserve
    @property (nonatomic, readwrite, copy) NSArray *dataSource;
    

    注意:RACObserve使用了KVO来监听property的变化,只要property被自己或外部改变,block就会被执行。但不是所有的property都可以被RACObserve,该property必须支持KVO,比如NSURLCachecurrentDiskUsage就不能被RACObserve。因为RAC是基于KVO的,NSMutableArray并不会在调用addObjectremoveObject时发送通知( willChangeValueForKey:didChangevlueForKey:),所以不可行。在使用RAC开发时,若要监听数组的变化,请将数组设计为不可变的数组(NSArray *dataSource),但是NSMutableArray也是可以添加KVO的 👉 详情请戳我

    1. 关于Cell复用时清理数据绑定或者事件监听的问题
    @implementation SUGoodsCell
     - (void) awakeFromNib {
        [super awakeFromNib];
        RAC(self.usernameLabel,  text) = RACObserve(self,  viewModel. username);
        RAC(self.userIdLabel,  text) = RACObserve(self,  viewModel. userId);
    }
    

    注意viewModel出现在RACObserve宏中逗号右边。 这些 cell 终将被重用,新的viewModels将会被赋值,如果我们不将 viewModel放在逗号右边,那就会监听viewModel属性的变化然后每次都要重新设置绑定;如果放在逗号右边, RACObserve 将会为我们负责这些事儿, 因此我们只需要设定一次绑定并让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来开发实践,毕竟萝卜白菜,各有所爱,目的就是便于大家更深层次的了解MVCMVVM的异同,以及提供一个利用MVVM真实开发的样例,希望能够打消大家对MVVM模式的顾虑。为了方便我们从宏观上了解功能的的整体结构,我们可以分别看看MVCMVVM Without ReactiveCococa 以及MVVM WithReactiveCococa 的类图。大家可以跟着类图,顺藤摸瓜,秉承该看的看,不该看的偷偷看的原则,赶快行动起来吧。

    七、期待

    文章若对您有点帮助,请给个喜欢❤️,毕竟码字不易;若对您没啥帮助,请给点建议💗,切记学无止境。
    针对文章所述内容,阅读期间任何疑问;请在文章底部批评指正,我会火速解决和修正问题。
    GitHub地址:https://github.com/CoderMikeHe

    八、参考链接

    相关文章

      网友评论

      • 周英俊a:您好有个问题请教下,我请求API 由于登录 超时了 需要显示一个UIAlertController 然后跳转到登录界面怎么写好?
        周英俊a:@CoderMikeHe 如果两个个对象同时执行同一个信号会有问题吗?,就比如说两个按钮同时点击发送请求,为了减少代码 订阅的同一个信号。
        周英俊a:@CoderMikeHe 好的 谢谢
        CoderMikeHe:@像一棵树生活着 这种你在VIewModel暴露一个属性,然后在控制器监听这个属性变化,来做处理即可。
      • Dimon_Hu:作者你好是否有QQ或者其他的联系方式?询问一个一个问题。
      • Lol刀妹:多谢分享,最近正在学习:smile:
      • zl520k:请问有后台服务吗?
        CoderMikeHe:@zl520k 哈哈,带我研究研究后台开发吧。前期勉强看看吧。:stuck_out_tongue_winking_eye:
        zl520k:@CoderMikeHe 我已经看到代码,最好能上后台
        CoderMikeHe:@zl520k 目前还没加后台,都是获取本地数据。当然你可以看看 https://github.com/CoderMikeHe/WeChat这个。这里面增加了网络获取数据。
      • 忘词_:大神 我最近在看你代码,你几年经验了
        CoderMikeHe:@那一抹口水 两年多了
      • f37810de84b1:哥们,你demo有个问题,RACCommand 的return nil是不对的,会crash,应该返回一个空的信号[RACSignal empty]
        CoderMikeHe:@三木桑 能否给个具体的代码位置给我,我去检查修改一下
      • 小番茄加西红柿:请问如何下载你的git上Demo,每次下载都出错解压不出来
        小番茄加西红柿:@CoderMikeHe :+1:
      • Johnny_Chang:一时间还消化不完,能写篇RAC介绍文章就更好了。
        CoderMikeHe:@Johnny_Chang 哈哈,RAC我也不太6,写出来别误导人。
      • Simple_Dev:写的太好了,太棒了,学习了,学习了!好好读你这文章!
        CoderMikeHe:哈哈哈 ,有用就可以了。

      本文标题:iOS 关于MVVM With ReactiveCocoa设计模

      本文链接:https://www.haomeiwen.com/subject/bfwwqxtx.html