ReactiveCocoa理解和使用1

作者: 王大吉Rock | 来源:发表于2017-08-31 14:44 被阅读22次

    写在前面

    作为一名快具备两年多工作经验的IOS开发者的我,在使用了oc 、swift、storyboard开发之后,难免觉得对单纯开发有点乏味,所以在以后的项目都需要有所改变。swift4.0在今年的九月份就要出正式版了,我想以后我可能几乎不会使用oc 来开发项目了,所以这可能是我最后一个oc项目了,额,我会以这样的方式来开心下----使用MVVC模式,并加上ReactiveCocoa响应式编程,这样可以增强对RxSwift的理解。

    温馨提示:RAC的编程方式虽然已经被很多人接纳了,但是在做项目的时候也要根据能力、项目的松紧程度、后期的维护来选择。当这样的一个项目交接给别人来维护时,对后期的维护人员的选择是有一定的难度的,每个人的编程方式是不一样的,而对RAC不熟的童鞋来说,入手是件比较辛苦的事情。

    进入正题

    RAC入门

    作为一个iOS开发者,你写的每一行代码几乎都是在响应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。

    RAC图文表示

    RAC可以简单理解成一下图
    http://www.jianshu.com/p/7fbd3453e0ee

    比较给力的教程参考文档

    https://www.raywenderlich.com/62699/reactivecocoa-tutorial-pt1

    感谢很多大神翻译的文章可以让更多的人快速入门ReactiveCocoa,以下是我的理解和补充


    RAC初体验

    • 醉醉醉基本的功能:
      IOS的事件处理,比如action、delegate、KVO、callback等,为事件定义了一个标准接口。
      一个demo展示demo

    谈论:
    RAC将action方法转成了block的形式

    action.png RAC.png

    似不似觉得代码瞬间变得高大上了。并且这只是RAC的很小的一个部分。RAC给几乎所有的UI控件添加了适用于ARC响应式编程的方法和属性:如下:

    RAC针对UI控件添加方法和属性.png
    1. 可以发现每一个UI控件都添加了RACSignal属性,控件的使用都是基于RACSignal属性的,都是由RACSignal发送事件流给它的subscriber。
    2. ReactiveCocoa框架使用category来为很多基本UIKit控件添加signal(RACSignal属性)。这样你就能给控件添加订阅了,text field的rac_textSignal就是这么来的。
    3. UI方面做了需要的封装成block的操作。

    可以理解成这样
    (1) ReactiveCocoa signal(RACSignal)发送事件流给它的subscriber。目前总共有三种类型的事件:next、error、completed。一个signal在因error终止或者完成前,可以发送任意数量的next事件。在这里,我们将会关注next事件。以后将会学习error和completed事件。
    (2) RACSignal有很多方法可以来订阅不同的事件类型。每个方法都需要至少一个block,当事件发生时就会执行block中的逻辑。在上面的例子中可以看到每次next事件发生时,subscribeNext:方法提供的block都会执行。

    • 添加过滤(filter过滤器)
      很多情况下,我们会要求当输入的字符长度超过n的时候,改变下背景颜色。
        // 只关心超过3个字符长度的用户名
       [[self.textField.rac_textSignal filter:^BOOL(id value) {
            NSString *text = value;
            self.textField.backgroundColor = [UIColor blueColor];
            return text.length > 3;
        }] subscribeNext:^(id x) {
            self.textField.backgroundColor = [UIColor redColor];
            NSLog(@"%@",x);
        }];
    

    也可以写成

        // 其他的写法
        // filter操作的输出也是RACSignal
        
        // 获取textField的信号
        RACSignal *textFieldSoureSignal = self.textField.rac_textSignal;
          
        // 获取textField的信号的过滤信号
        RACSignal *filteredTextField = [textFieldSoureSignal filter:^BOOL(id value) {
            // 过滤条件
            NSString *text = value;
            return text.length > 3;
        }];
        
        // 过滤信号的操作
        [filteredTextField subscribeNext:^(id x) {
            NSLog(@"%@",x);
        }];
    

    RACSignal的每个操作都会返回一个RACsignal,这在术语上叫做连贯接口(fluent interface)。这个功能可以让你直接构建管道,而不用每一步都使用本地变量。

    注意:ReactiveCocoa大量使用block。如果你是block新手,你可能想看看Apple官方的block编程指南。如果你熟悉block,但是觉得block的语法有些奇怪和难记,你可能会想看看这个有趣又实用的网页f*****gblocksyntax.com

    • 事件类型
      刚刚在代码中使用的filter、Next都是事件类型。现在添加map事件。
    // 添加map操作
        [[[self.textField.rac_textSignal
           map:^id(NSString *text) {
               return @(text.length);
           }]
          filter:^BOOL(NSNumber *length) {
            return [length integerValue] > 3;
          }]
          subscribeNext:^(NSString *x) {
             NSLog(@"%@",x);
        }];
    

    log输出:


    map操作log输出.png

    新加的map操作通过block改变了事件的数据。map从上一个next事件接收数据,通过执行block把返回值传给下一个next事件。在上面的代码中,map以NSString为输入,取字符串的长度,返回一个NSNumber,在接下来的管道中如果不修改类型都是以NSNumber传递下去。

    map操作将修改事件的数据.png
    • 创建有效状态信号
      创建信号,标识输入框的输入内容是否有效。
        RACSignal *validTextFieldSignal = [self.textField.rac_textSignal map:^id(NSString *text) {
            return @([self isValidTextField:text]);
        }];
    

    上面的代码对每个输入框的rac_textSignal应用了一个map转换。输出是一个用NSNumber封装的布尔值。

    下一步是转换这些信号,从而能为输入框设置不同的背景颜色。基本上就是,你订阅这些信号,然后用接收到的值来更新输入框的背景颜色。

        [[validTextFieldSignal map:^id(NSNumber *num) {
            return [num boolValue] ? [UIColor redColor] : [UIColor blueColor];
        }]
         subscribeNext:^(UIColor *color) {
             self.textField.backgroundColor = color;
        }];
    

    推荐下面的宏的写法:

    // RAC宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。
        RAC(self.textField, backgroundColor) = [validTextFieldSignal map:^id(NSNumber *num) {
            return [num boolValue] ? [UIColor redColor] : [UIColor blueColor];
        }];
    

    RAC宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。

    • 聚合信号
      举个例子,当用户名、密码输入框的输入内容都有效时登录按钮才能点击。现在要把这里改成响应式的。
        // 输入框
        RACSignal *validTextFieldSignal = [self.textField.rac_textSignal map:^id(NSString *text) {
            return @([self isValidTextField:text]);
        }];
        // 输入框1
        RACSignal *validTextFieldSignal1 = [self.textField1.rac_textSignal map:^id(NSString *text) {
            return @([self isValidTextField:text]);
        }];
        
        //合并信号
        //每当validTextFieldSignal和validTextFieldSignal1变化的时候,都会产生一个新的信号return出来
        RACSignal *signupActiveSignal = [RACSignal combineLatest:@[validTextFieldSignal, validTextFieldSignal1] reduce:^id(NSNumber *validTextField, NSNumber *validTextField1){
            return @([validTextField boolValue] && [validTextField1 boolValue]);
        }];
        
        // 新的信号变化,就会进行subscribeNext操作
        [signupActiveSignal subscribeNext:^(NSNumber *signupActive) {
            self.loginBtn.enabled = [signupActive boolValue];
        }];
    
    RAC合并概念.png

    上图展示了一些重要的概念,你可以使用ReactiveCocoa来完成一些重量级的任务。

    • 分割——信号可以有很多subscriber,也就是作为很多后续步骤的源。注意上图中那个用来表示用户名和密码有效性的布尔信号,它被分割成多个,用于不同的地方。
    • 聚合——多个信号可以聚合成一个新的信号,在上面的例子中,两个布尔信号聚合成了一个。实际上你可以聚合并产生任何类型的信号。

    代码中没有用来表示两个输入框有效状态的私有属性。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。

    • 创建信号
      现在就可以点击登录按钮了,按钮按下的处理ReactiveCocoa提供了rac_signalForControlEvents方法可以实现。
        // 点击登录
        [[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *loginBtn) {
            NSLog(@"%@",loginBtn);
        }];
    

    现在我们可以将登录的异步API(封装好的网络请求)放在回调方法中执行,这样就可以完成登录的要求了。

    • 思考一下,如果把所谓的登录的异步API信号的方式来表示,那么代码会是什么样子?
      我们可以使用map操作将登录的异步API转成获取到了登录结果的“信号”
      (可以想成经过登录操作后异步回调的结果,可能就是一个布尔值),那就可以写成事件流的形式。

    创建获取到了登录结果的“信号”

    // 创建登录信号
    - (RACSignal *)signInSignal {
        
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            [[[RWDummySignInService alloc] init] signInWithUsername:self.textField.text
                                                           password:self.textField.text
                                                           complete:^(BOOL success) {
                                                               // 发送登录的结果
                                                               [subscriber sendNext:@(success)];
                                                               [subscriber sendCompleted];
            }];
            return nil;
        }];
    }
    

    那么代码就可以写成这样:

     // (将按钮的点击信号 转成 登录信号)
        [[[self.loginBtn
           rac_signalForControlEvents:UIControlEventTouchUpInside]
          // 将按钮的点击信号-->登录信号
          map:^id(id value) {
              // 后续传递的都是登录信号
              return [self signInSignal];
          }] subscribeNext:^(id x) {
              // next操作中输出的是登录信号(而不是登录的结果)
              NSLog(@"%@",x);
          }];
    

    上面的代码使用map方法,把按钮点击信号转换成了登录信号,subscriber输出的log。输出的是一个信号对象(这是什么鬼)

    map方法.png

    上面问题的解决方法,有时候叫做信号中的信号,换句话说就是一个外部信号里面还有一个内部信号。你可以在外部信号的subscribeNext:block里订阅内部信号。不过这样嵌套太混乱啦,还好ReactiveCocoa已经解决了这个问题。(其实我并不知道应该怎么写)

    • 信号中的信号
      解决的方法很简单,只需要把map操作改成flattenMap就可以了:
        // (将按钮的点击信号 转成 登录信号)
        [[[self.loginBtn
           rac_signalForControlEvents:UIControlEventTouchUpInside]
          // 将按钮的点击信号-->登录信号
          flattenMap:^id(id value) {
              // 后续传递的都是登录信号
              return [self signInSignal];
          }] 
          subscribeNext:^(id x) {
              // next操作中输出的是登录结果
              NSLog(@"%@",x);
          }];
    

    可以查看下内部源码就会发现map操作是基于flattenMap


    map操作.png
    • 添加附加操作(Adding side-effects)
      你注意到这个应用现在有一些用户体验上的小问题了吗?当登录service正在校验用户名和密码时,登录按钮应该是不可点击的。这会防止用户多次执行登录操作。还有,如果登录失败了,用户再次尝试登录时,应该隐藏错误信息。

    doNext:是直接跟在按钮点击事件的后面。而且doNext: block并没有返回值。因为它是附加操作,并不改变事件本身。
    之前的管道图就更新成下面这样的:

    doNext操作.png

    注意:在异步操作执行的过程中禁用按钮是一个常见的问题,ReactiveCocoa也能很好的解决。RACCommand就包含这个概念,它有一个enabled信号,能让你把按钮的enabled属性和信号绑定起来。你也许想试试这个类。

    相关文章

      网友评论

        本文标题:ReactiveCocoa理解和使用1

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