Notes and thoughts from Tony

ReactiveCocoa简单介绍

Comments

ReactiveCocoa简介

ReactiveCocoa is inspired by functional reactive programming. Rather than using mutable variables which are replaced and modified in-place, RAC offers “event streams,” represented by the Signal and SignalProducer types, that send values over time.

ReactiveCocoa的灵感来自函数式响应式编程(FRP)。RAC并不采用随时可变的变量,而是用事件流(表现为SignalSignalProducer)的方式来捕捉值的变化。

Event streams unify all of Cocoa’s common patterns for asynchrony and event handling, including:

  • Delegate methods
  • Callback blocks
  • NSNotifications
  • Control actions and responder chain events
  • Futures and promises
  • Key-value observing (KVO)

在我们iOS开发过程中,经常会响应某些事件来处理某些业务逻辑,例如按钮的点击,上下拉刷新,网络请求,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。

其实这些事件,都可以通过RAC处理,ReactiveCocoa为事件提供了很多处理方法,而且利用RAC处理事件很方便,可以把要处理的事情,和监听的事情的代码放在一起,这样非常方便我们管理,就不需要跳到对应的方法里。非常符合我们开发中高聚合,低耦合的思想。

ReactiveCocoa常见类

RACSignal

信号类,一般表示将来有数据传递,只要有数据改变,信号内部接收到数据,就会马上发出数据。注意是,数据发出,并不是信号类发出。

  • 信号类(RACSignal),只是表示当数据改变时,信号内部会发出数据,它本身不具备发送信号的能力,而是交给内部一个订阅者去发出。

  • 默认一个信号都是冷信号,也就是值改变了,也不会触发,只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。

  • 如何订阅信号:调用信号RACSignal的subscribeNext就能订阅。

RACSignal使用步骤

  1. 创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
  2. 发送信号 - (void)sendNext:(id)value

  3. 订阅信号,才会激活信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock

RACSignal底层实现

  1. 创建子类信号RACDynamicSignal,首先把didSubscribe这个block保存到信号RACDynamicSignal中,但是还不会触发(冷信号)。

  2. 当信号被订阅,也就是调用signalsubscribeNext:nextBlocksubscribeNext内部会创建订阅者subscriber,并且把nextBlock保存到订阅者subscriber中,此时为热信号。

  3. subscribeNext内部会调用RACDynamicSignaldidSubscribe这个block。通常也就是在RACDynamicSignaldidSubscribe中调用[subscriber sendNext:@1]

  4. sendNext底层其实就是执行订阅者subscribernextBlock

  5. 标记信号发送完成或者取消订阅。

  6. 执行RACDisposabledisposeBlock中的代码。

代码:

// 1.创建信号 createSignal:didSubscribe(block)
// RACDisposable:取消订阅
// RACSubscriber:发送数据
RACSignal *siganl = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
   
   // block调用时刻:每当有订阅者订阅信号,就会调用block。
   // block作用:描述当前信号哪些数据需要发送
   // _subscriber = subscriber;

   // 3.发送信号
   NSLog(@"调用了didSubscribe");
   // 通常:传递数据出去
   [subscriber sendNext:@1];
   // 此处调用订阅者的nextBlock
   
   // 5.如果不再发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。或者信号想要被取消,就必须返回一个RACDisposable,然后在后面[disposable dispose]
   [subscriber sendCompleted];
   
   return [RACDisposable disposableWithBlock:^{
       // 6.信号什么时候被取消:1.自动取消,当一个信号的订阅者被销毁的时候,就会自动取消订阅 2.主动取消
       // block调用时刻:一旦一个信号,被取消订阅的时候就会调用
       // block作用:当信号取消订阅,用于清空一些资源
       NSLog(@"取消订阅");
   }];
}];
    
// subscribeNext:创建订阅者,然后把nextBlock保存到订阅者里面
// 2.订阅信号:只要订阅信号,就会返回一个取消订阅信号的类
[siganl subscribeNext:^(id x) {
   // 4.block调用时刻:每当有信号发出数据,就会调用block。
   NSLog(@"接收到数据:%@",x);
}];
    
// 5.取消订阅
//    [disposable dispose];
2016-03-19 21:51:02.587 RACDemo[44522:5148595] 调用了didSubscribe
2016-03-19 21:51:02.588 RACDemo[44522:5148595] 接收到数据:1
2016-03-19 21:51:02.589 RACDemo[44522:5148595] 取消订阅

RACSubscriber

表示订阅者的意思,用于发送信号,这是一个协议,不是一个类,只要遵守这个协议,并且实现方法才能成为订阅者。通过create创建的信号,都有一个订阅者,帮助他发送数据。

RACDisposable

用于取消订阅或者清理资源,当信号发送完成或者发送错误的时候,就会自动触发它。

RACSubject

信号提供者,自己可以充当信号,又能发送信号。

使用场景:通常用来代替代理,有了它,就不必要定义代理了。

RACSubject使用步骤

  1. 创建信号 [RACSubject subject],跟RACSignal不一样,创建信号时没有block。

  2. 订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock

  3. 发送信号 sendNext:(id)value

RACSubject:底层实现和RACSignal不一样。

  1. 调用subscribeNext订阅信号,只是把订阅者保存起来,并且订阅者的nextBlock已经赋值了。

  2. 调用sendNext发送信号,遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock

代码:

// RACSubject:信号提供者
    
// 1.创建信号
RACSubject *subject = [RACSubject subject];
    
// 2.订阅信号
[subject subscribeNext:^(id x) {
   
   // 4.block:当有数据发出的时候就会调用
   // block:处理数据
   NSLog(@"第一个订阅者%@",x);
}];
    
// 发送信号
//    [subject sendNext:@1];
    
// 2.1 第二次订阅信号
[subject subscribeNext:^(id x) {
   // block调用时刻:当信号发出新值,就会调用.
   NSLog(@"第二个订阅者%@",x);
}];
    
// 3.发送信号
[subject sendNext:@1];
[subject sendNext:@2];
2016-03-19 22:00:09.573 RACDemo[44558:5150846] 第一个订阅者1
2016-03-19 22:00:09.574 RACDemo[44558:5150846] 第二个订阅者1
2016-03-19 22:00:09.575 RACDemo[44558:5150846] 第一个订阅者2
2016-03-19 22:00:09.575 RACDemo[44558:5150846] 第二个订阅者2

RACSubject替换代理:

//在SubjectSecondViewController.h中添加一个RACSubject代替代理。
@property (nonatomic, strong) RACSubject *subject;

//监听SubjectSecondViewController的按钮点击
- (IBAction)backDidClick:(UIButton *)sender {
    
    // 4.点击返回按钮时,便可以通知SubjectViewController做事情
    if (self.subject) {
        
        [self.subject sendNext:@1];
    }
}

// 在SubjectViewController中,监听跳转按钮,给SubjectSecondViewController的代理信号赋值,并且监听
- (IBAction)buttonDidClick:(UIButton *)button {
    
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    
    SubjectSecondViewController *twoVc = [storyboard instantiateViewControllerWithIdentifier:@"two"];
    
    //1.创建并设置信号提供者
    twoVc.subject = [RACSubject subject];
    
    // 2.订阅信号
    @weakify(twoVc)
    [twoVc.subject subscribeNext:^(id x) {
        
        NSLog(@"通知了ViewController:%@", x);
        @strongify(twoVc)
        [twoVc dismissViewControllerAnimated:YES completion:^{
            
        }];
    }];
    
    // 3.present控制器
    [self presentViewController:twoVc animated:YES completion:nil];
    
}

RACReplaySubject

重复提供信号类,RACSubject的子类。

使用场景一:如果一个信号每被订阅一次,就需要把之前的值重复发送一遍,使用重复提供信号类。

使用场景二:可以设置capacity数量来限制缓存的value的数量,即只缓充最新的几个值。

  1. 创建信号 [RACReplaySubject subject],跟RACSignal不一样,创建信号时没有block。
  2. 可以先订阅信号,也可以先发送信号。
    1. 订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
    2. 发送信号 sendNext:(id)value

RACReplaySubject:底层实现和``RACSubject不一样。

  1. 调用sendNext发送信号,把值保存起来,然后遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。
  2. 调用subscribeNext订阅信号,遍历保存的所有值,一个一个调用订阅者的nextBlock

如果想当一个信号被订阅,就重复播放之前所有值,需要先发送信号,再订阅信号。也就是先保存值,再订阅值

代码:

// 1.创建信号
RACReplaySubject *subject = [RACReplaySubject subject];
    
// 2.订阅信号
[subject subscribeNext:^(id x) {
   
   NSLog(@"第一个订阅者%@",x);
}];
    
// 3.发送信号
[subject sendNext:@1];
    
[subject sendNext:@2];
    
[subject subscribeNext:^(id x) {
   
   NSLog(@"第二个订阅者%@",x);
}];
2016-03-19 22:02:33.387 RACDemo[44558:5150846] 第一个订阅者1
2016-03-19 22:02:33.387 RACDemo[44558:5150846] 第一个订阅者2
2016-03-19 22:02:33.389 RACDemo[44558:5150846] 第二个订阅者1
2016-03-19 22:02:33.390 RACDemo[44558:5150846] 第二个订阅者2

从输出中看出,无论sendNext在订阅之前还是之后,输出不变。而RACSubject同样的代码顺序则会输出:

2016-03-19 22:04:41.971 RACDemo[44572:5151777] 第一个订阅者1
2016-03-19 22:04:41.972 RACDemo[44572:5151777] 第一个订阅者2

也就是在sendNext后面订阅的信号已经不管用了。

RACTuple

元组类,类似NSArra有,用来包装值。

RACTupleUnpack宏:专门用来解析元组。等号右边:需要解析的元组。宏的参数:填解析数据的类型。元组里面有几个值,宏的参数就必须填几个。

// 遍历字典,遍历出来的键值对会包装成RACTuple(元组对象)
NSDictionary *dict = @{@"name":@"Tony", @"age":@18};
[dict.rac_sequence.signal subscribeNext:^(RACTuple *x) {
   // 解包元组,会把元组的值,按顺序给参数里面的变量赋值
   RACTupleUnpack(NSString *key, NSString *value) = x;
   // 相当于以下写法
   // NSString *key = x[0];
   // NSString *value = x[1]; 
   NSLog(@"%@ %@", key, value);
}];
2016-03-19 22:38:18.100 RACDemo[44687:5159036] name Tony
2016-03-19 22:38:18.105 RACDemo[44687:5159036] age 18

RACSequence

RAC中的集合类,用于代替NSArray和NSDictionary,可以使用它来快速遍历数组和字典。可以用来字典转模型等。

// 遍历数组
NSArray *arr = @[@1,@2,@3];
    
// 这里其实是三步
    
// 第一步: 把数组转换成集合RACSequence arr.rac_sequence
// 第二步: 把集合RACSequence转换RACSignal信号类,arr.rac_sequence.signal
// 第三步: 订阅信号,激活信号,会自动把集合中的所有值,遍历出来。
[arr.rac_sequence.signal subscribeNext:^(id x) {
   NSLog(@"%@",x);
}];
2016-03-19 22:34:23.459 RACDemo[44674:5158285] 1
2016-03-19 22:34:23.460 RACDemo[44674:5158285] 2
2016-03-19 22:34:23.461 RACDemo[44674:5158285] 3

字典转模型:

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];
NSArray *dictArr = [NSArray arrayWithContentsOfFile:filePath];

//OC
NSMutableArray *items = [NSMutableArray array];
for (NSDictionary *dict in dictArr) {
   FlagItem *item = [FlagItem flagWithDict:dict];
   [items addObject:item];
}
    
//RAC
NSMutableArray *flags = [NSMutableArray array];
// rac_sequence注意点:调用subscribeNext,并不会马上执行nextBlock,而是会等一会。
[dictArr.rac_sequence.signal subscribeNext:^(id x) {
   // 运用RAC遍历字典,x:字典
   FlagItem *item = [FlagItem flagWithDict:x];
   [flags addObject:item];
}];
    
// 使用map
// map:映射的意思,目的:把原始值value映射成一个新值
// array: 把集合转换成数组
// 底层实现:当信号被订阅,会遍历集合中的原始值,映射成新值,并且保存到新的数组里。
NSArray *newFlags = [[dictArr.rac_sequence map:^id(id value) {
   return [FlagItem flagWithDict:value];
}] array];
    
NSLog(@"%@, %@, %@", items, flags, newFlags);

RACCommand

RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。

使用场景:监听按钮点击,网络请求。

  • RACCommand使用步骤:
  1. 创建命令 initWithSignalBlock:(RACSignal * (^)(id input))signalBlock
  2. signalBlock中,创建RACSignal,并且作为signalBlock的返回值
  3. 执行命令 - (RACSignal *)execute:(id)input
  • RACCommand使用注意:
  1. signalBlock必须要返回一个信号,不能传nil。
  2. 如果不想要传递信号,直接创建空的信号[RACSignal empty]
  3. RACCommand中信号如果数据传递完,必须调用[subscriber sendCompleted],这时命令才会执行完毕,否则永远处于执行中。
  4. RACCommand需要被强引用,否则接收不到RACCommand中的信号,因此RACCommand中的信号是延迟发送的。
  • RACCommand设计思想:内部signalBlock为什么要返回一个信号,这个信号有什么用。
  1. 在RAC开发中,通常会把网络请求封装到RACCommand,直接执行某个RACCommand就能发送请求。
  2. RACCommand内部请求到数据的时候,需要把请求的数据传递给外界,这时候就需要通过``signalBlock返回的信号传递了。
  • 如何拿到RACCommand中返回信号发出的数据。
  1. RACCommand有个执行信号源executionSignals,这个是``signal of signals(信号的信号),意思是信号发出的数据是信号,不是普通的类型。
  2. 订阅``executionSignals就能拿到RACCommand中返回的信号,然后订阅signalBlock返回的信号,就能获取发出的值。
  • 监听当前命令是否正在执行executing。
// 创建命令类
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
   
   // block什么时候调用:当执行这个命令类的时候就会调用
   NSLog(@"执行命令 %@", input);
   // block有什么作用:描述下如何处理事件,网络请求
   
   // 创建空信号,必须返回信号
   // return [RACSignal empty];
   
   // 2.RACCommand必须返回信号,处理事件的时候,肯定会有数据产生,产生的数据就通过返回的信号发出。注意:数据传递完,最好调用sendCompleted,这时命令才执行完毕。
   return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
       // block作用:发送处理事件的信号
       // block调用:当信号被订阅的时候才会调用
       [subscriber sendNext:@"信号发出的内容"];
       [subscriber sendCompleted];
       
       return nil;
   }];
}];
    
// 强引用命令,不要被销毁,否则接收不到数据
_command = command;
// executionSignals:信号源,包含事件处理的所有信号。
// executionSignals: signalOfSignals,信号中的信号,就是信号发出的数据也是信号类
    
// 3.如果想要订阅接收信号源的信号内容,必须保证命令类不会被销毁
[command.executionSignals subscribeNext:^(id x) {
   // x -> 信号
   [x subscribeNext:^(id x) {
       
       NSLog(@"%@",x);
   }];
}]; 
// 4.执行命令,调用signalBlock
[command execute:@1];
     
// RAC高级用法
// switchToLatest:用于signal of signals,获取signal of signals发出的最新信号,也就是可以直接拿到RACCommand中的信号
[command.executionSignals.switchToLatest subscribeNext:^(id x) {
   NSLog(@"%@",x);
}];
    
// 5.监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次信号。
[[command.executing skip:1] subscribeNext:^(id x) {
   if ([x boolValue] == YES) {
       // 正在执行
       NSLog(@"正在执行");
       
   }else{
       // 执行完成
       NSLog(@"执行完成");
   }
}];

RACMulticastConnection

用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理。

使用注意:RACMulticastConnection通过RACSignal- (RACMulticastConnection *)publish或- (RACMulticastConnection *)multicast:(RACSubject *)subject方法创建.

  • RACMulticastConnection使用步骤:
  1. 创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
  2. 创建连接 RACMulticastConnection *connect = [signal publish];
  3. 订阅信号。注意:订阅的不在是之前的信号,而是连接的信号。 [connect.signal subscribeNext:nextBlock]
  4. 连接 [connect connect]
  • RACMulticastConnection底层原理:
  1. 创建connectconnect.sourceSignal -> RACSignal(原始信号) connect.signal -> RACSubject
  2. 订阅connect.signal,会调用RACSubject的subscribeNext,创建订阅者,而且把订阅者保存起来,不会执行block。
  3. [connect connect]内部会订阅RACSignal(原始信号),并且订阅者是RACSubject。
    1. 订阅原始信号,就会调用原始信号中的didSubscribe
    2. didSubscribe,拿到订阅者调用sendNext,其实是调用RACSubject的`sendNext
  4. RACSubjectsendNext,会遍历RACSubject所有订阅者发送信号。
    1. 因为刚刚第二步,都是在订阅RACSubject,因此会拿到第二步所有的订阅者,调用他们的nextBlock

需求:假设在一个信号中发送请求,每次订阅一次都会发送请求,这样就会导致多次请求。 解决:使用RACMulticastConnection就能解决.

// 发送请求,用一个信号内包装,不管有多少个订阅者,只想要发送一次请求
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
   // didSubscribe(block)中的代码都统称为副作用(Side Effects)。
   // 发送请求
   NSLog(@"发送请求");
   [subscriber sendNext:@1];
   return nil;
}]; 
// 订阅信号
[signal subscribeNext:^(id x) {
   NSLog(@"接收数据:%@",x);
}];
[signal subscribeNext:^(id x) {
   NSLog(@"接收数据:%@",x);
}];
2016-03-20 13:02:59.130 RACDemo[45388:5236769] 发送请求
2016-03-20 13:02:59.134 RACDemo[45388:5236769] 接收数据:1
2016-03-20 13:02:59.134 RACDemo[45388:5236769] 发送请求
2016-03-20 13:02:59.135 RACDemo[45388:5236769] 接收数据:1

运行结果,会执行两遍发送请求,也就是每次订阅都会发送一次请求。

// RACMulticastConnection:解决重复请求问题
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
   NSLog(@"发送请求");
   [subscriber sendNext:@1];
   return nil;
}];
// 2.创建连接
RACMulticastConnection *connect = [signal publish];
// 3.订阅信号,
// 注意:订阅信号,也不能激活信号,只是保存订阅者到数组,必须通过连接,当调用连接,就会一次性调用所有订阅者的sendNext:
[connect.signal subscribeNext:^(id x) {
   NSLog(@"订阅者一信号");
}];
[connect.signal subscribeNext:^(id x) {
   NSLog(@"订阅者二信号");
}];
// 4.连接,激活信号
[connect connect];
2016-03-20 13:05:13.803 RACDemo[45398:5237395] 发送请求
2016-03-20 13:05:13.804 RACDemo[45398:5237395] 订阅者一信号
2016-03-20 13:05:13.804 RACDemo[45398:5237395] 订阅者二信号

RACSchedule

RAC中的队列,用GCD封装的。

RACUnit

表⽰stream不包含有意义的值,也就是看到这个,可以直接理解为nil。

RACEvent

把数据包装成信号事件(signal event)。它主要通过RACSignal的-materialize来使用。

参考:

Framework Overview

最快让你上手ReactiveCocoa之基础篇

代码:

文章中的代码都可以从我的GitHub RACDemo找到。