首页投稿(暂停使用,暂停投稿)

ReactiveCocoa理解和使用1

2017-08-31  本文已影响22人  王大吉Rock

写在前面

作为一名快具备两年多工作经验的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初体验

谈论:
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都会执行。

    // 只关心超过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

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

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

    // 点击登录
    [[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已经解决了这个问题。(其实我并不知道应该怎么写)

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

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


map操作.png

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

doNext操作.png

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

上一篇下一篇

猜你喜欢

热点阅读