iOS ReactiveCocoa基础使用

1、ReactiveCocoa简介

ReactiveCocoa(简称为RAC),是由Github 开源的一个应用于iOS和OS X开发的新框架。RAC具有函数式编程(FP)和响应式编程(RP)的特性。所以,RAC也被描述为函数响应式编程(FRP)框架。 RAC框架的核心类是RACSignal, 所有的消息都是通过信号的方式进行传递,可以简单的理解这些信号就是一连串的状态,在状态发生改变的时候,对应的订阅者就会收到通知,然后执行相应的操作。

响应式编程思想:不需要考虑调用顺序,只需要知道考虑结果。类似于蝴蝶效应,产生一个事件,会影响很多东西。这些事件像流一样的传播出去,然后影响结果,借用面向对象的一句话,万物皆是流。

比如在程序开发中 a = b + c ,赋值之后 b 或者 c 的值变化后,a 的值不会跟着变化。而采用响应式编程,则如果 b 或者 c 的数值发生变化,a 的数值会同时发生变化,即 a 的值与 b 和 c 绑定。

2、ReactiveCocoa主要应用场景

UI数据绑定

RAC可以方便的绑定任何数据流到UI控件上。

用户交互事件绑定

RAC可以为可交互的UI控件提供了一系列能发送Signal信号的方法。

统一的消息传递机制

RAC提供了统一的方法去处理异步的行为,包括delegate方法、blocks回调、target-action机制、notifications和KVO。

3、ReactiveCocoa常见类

思维导图

ReactiveCocoa思维导图.png

常见类

1、RACSiganl

信号类。只是表示当数据改变时,信号内部会发出数据,它本身不具备发送信号的能力,而是交给内部一个订阅者去发出。默认信号都是冷信号,也就是值改变了,也不会触发。只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。如何订阅信号:调用信号RACSignal的subscribeNext就能订阅

RACEmptySignal :空信号,用来实现 RACSignal 的 +empty 方法;
RACDynamicSignal :动态信号,使用一个 block 来实现订阅行为,我们在使用 RACSignal 的 +createSignal: 方法时创建的就是该类的实例;
RACErrorSignal :错误信号,用来实现 RACSignal 的 +error: 方法;

2、RACSubscriber

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

3、RACDisposable

用于取消订阅或者清理资源,当信号发送完成或者发送错误的时候,就会自动触发它,也可以手动调用 [disposable dispose] 触发。

RAC使用流程

1、创建信号

2、订阅信号

3、发送信号

4、销毁信号 (需要的时候)

示例如下:

//1、创建信号(冷信号)
RACSignal * single = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
    //block调用时刻:每当有订阅者订阅信号,就会调用block
    
    //3、发送信号
    [subscriber sendNext:@"发送信息"];
    
    //如果不再发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅
    [subscriber sendCompleted];
    
    return [RACDisposable disposableWithBlock:^{
       //block调用时刻:当信号发送完成或者发送错误,就会自动执行这个block,取消订阅。执行完Block后,当前信号就不在被订阅了
        NSLog(@"信号被销毁");
    }];
}];
    
 //2、订阅信号(该信号变成热信号)
 RACDisposable *disposable = [single subscribeNext:^(id  _Nullable x) {
     //block调用时刻:每当有信号发送数据,就会调用该方法
     NSLog(@"接收到数据:%@",x);
 }];
    
 //主动触发取消订阅
 //[disposable dispose];

4、RACSubject

上面说到 RACSignal 是不具备发送信号的能力的,但是RACSubject这个类就可以做到订阅/发送为一体。

使用场景:通常用来代替代理。

示例如下:

//1创建信号
RACSubject * subject = [RACSubject subject];
    
//2订阅信号 
[subject subscribeNext:^(id  _Nullable x) {
   NSLog(@"%@",x);
}];
   
//3发送数据
[subject sendNext:@"发送数据"];

5、RACCommand

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

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

示例如下:

//1、创建命令
RACCommand * loginBtnCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
    NSLog(@"接收传过来的登录请求参数:%@",input);
            
    //创建信号
    return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
       NSLog(@"开始请求");
       NSLog(@"请求中...");
       NSLog(@"请求成功");
       NSLog(@"处理数据");
                
       //发送信号
       [subscriber sendNext:@"我是请求到的数据"];
       //请求完成
       [subscriber sendCompleted];
                
       return  [RACDisposable disposableWithBlock:^{
           NSLog(@"结束了");
       }];
    }];
}];
        
/*
2、订阅loginBtnCommand命令事件中的信号发出的值
switchToLatest表示的是最新发送的信号
*/
[loginBtnCommand.executionSignals.switchToLatest subscribeNext:^(id  _Nullable x) {
     NSLog(@"接收到的请求数据:%@",x);
     NSLog(@"登录成功,跳转页面");
}];
        
/*
3、监听loginBtnCommand监听命令是否执行完毕,默认会来一次,可以直接跳过
默认会来一次,可以直接跳过,skip表示跳过第一次命令)
*/
[[loginBtnCommand.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) {
    if ([x boolValue]) {
        NSLog(@"正在执行中...");
    } else {
      NSLog(@"执行结束");
    }
}];
        
//4、执行命令
[loginBtnCommand execute:@{@"account":@"1234567890",@"password":@"123456"}];

6、RACSequence

RAC中的集合类,用于代替NSArray、NSDictionary。

使用场景:可以使用它来快速遍历数组和字典。

示例如下:

//遍历数组
NSArray * arr = @[@"1",@"2",@"3",@"4",@"5"];
/*
第一步: 把数组转换成集合RACSequence             numbers.rac_sequence
第二步: 把集合RACSequence转换RACSignal信号类    numbers.rac_sequence.signal
第三步: 订阅信号,激活信号,会自动把集合中的所有值,遍历出来。  
*/
[arr.rac_sequence.signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"内容:%@",x);
}];

//遍历字典,遍历出来的键值对会包装成RACTuple(元组对象)
NSDictionary *dict = @{@"name":@"xiaoming",@"age":@18};
[dict.rac_sequence.signal subscribeNext:^(id x) {
     //解包元组(x是元祖)
     RACTupleUnpack(NSString *name,NSString *age) = x;
     NSLog(@"%@ %@",name,age);
}];

7、RACScheduler

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

示例如下:

//定时器
@weakify(self)
RACDisposable * disposable = [[RACSignal interval:2 onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) {
   @strongify(self)
   NSLog(@"当前时间:%@",x);
   //关闭定时器
    [disposable dispose];
}];

8、RACMulticastConnection

信号连接类。用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理。RACMulticastConnection通过RACSignal的-publish或者-muticast:方法创建。

示例如下:

/*
RACMulticastConnection:信号连接类
一般情况下,信号被订阅多少次,信号创建的block就被调用多少次。但是实际开发中,block只需调用一次就可以了。RACMulticastConnection可以实现不管订阅多少次信号,信号的block只调用一次。
     
如下代码:信号被订阅了3次,但是singal里面的block只被调用一次,即只输出一次“请求数据”
*/
//1、创建信号
RACSignal * singal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
     NSLog(@"请求数据");
     [subscriber sendNext:@"请求数据"];
     return nil;
}];

//2、把信号转成连接类
RACMulticastConnection * connection = [singal publish];

//3、订阅连接类信号
[connection.signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"订阅1:%@",x);
}];

[connection.signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"订阅2:%@",x);
}];

[connection.signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"订阅3:%@",x);
}];

//4、连接,激活信号
[connection connect];

9、ReactiveCocoa常见宏

1、RAC(TARGET, [KEYPATH, [NIL_VALUE]]):用于给某个对象的某个属性绑定。

//这里吧label的text属性绑定到textField改变信号中,textfield的内容发生改变的时候就会发出信号,label的text就会跟随着改变。
RAC(self.label,text) = _textfield.rac_textSignal;

2、RACObserve(self, name):监听某个对象的某个属性,返回的是信号。相当于kvo使用。

//监听self的age属性,x为属性的信息
[RACObserve(self, age) subscribeNext:^(id  _Nullable x) {
    NSLog(@"x:%@",x);
}];

3、RACTuplePack:把数据包装成RACTuple(元组类)

4、RACTupleUnpack:把RACTuple(元组类)解包成对应的数据

//元组
RACTuple * tuple = RACTuplePack(@1,@2,@3);//使用RACTuplePack宏来快速创建
NSLog(@"TUPLE:%@",tuple);
    
//使用RACTupleUnpack宏快速解包
RACTupleUnpack(NSNumber *num1,NSNumber *num2,NSNumber *num3) = tuple;
NSLog(@"num1:%@",num1);
NSLog(@"num2:%@",num2);
NSLog(@"num3:%@",num3);
    
//使用下标的方式来获取
NSLog(@"第0个:%@",tuple[0]);

10、 ReactiveCocoa常见用法

1、rac_signalForSelector:用于替代代理。

/*
代替代理
rac_signalForSelector:把调用某个对象的方法的信息转换成信号,调用这个方法,就会发送信号。
当redView中的testButton按钮被点击了(即调用了testButtonClick),就会发出信号
*/
[[redView rac_signalForSelector:@selector(testButtonClick:)] subscribeNext:^(id x) {
    NSLog(@"redView中的testButton按钮被点击了");
}];

2、rac_valuesAndChangesForKeyPath:代替KVO,用于监听某个对象的属性改变。

/*
KVO
把监听redView的center属性改变转换成信号,只要值改变就会发送信号
x为属性的信息
*/
[[redView rac_valuesAndChangesForKeyPath:@"center" options:NSKeyValueObservingOptionNew observer:nil] subscribeNext:^(id x) {
    NSLog(@"center:%@",x);
}];

3、rac_signalForControlEvents:用于监听某个事件。

/*
监听事件
把按钮点击事件转换为信号,点击按钮,就会发送信号
x为Button的信息
*/
[[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
    NSLog(@"按钮被点击了");
}];

4、rac_addObserverForName:用于监听某个通知。

/*
代替通知
把监听到的通知转换信号
*/
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillShowNotification object:nil] subscribeNext:^(id x) {
    NSLog(@"键盘弹出");
}];

5、rac_textSignal:监听文本框文字改变。

//监听文本框的文字改变
[self.passwordTextField.rac_textSignal subscribeNext:^(NSString * _Nullable x) {
      NSLog(@"密码框输入的内容%@",x);
}];

11、ReactiveCocoa组合用法

1、concat:按一定顺序拼接信号,当多个信号发出的时候,有顺序的接收信号

//当需要按顺序执行的时候: 先执行C, 在执行D
RACSubject *subC = [RACSubject subject];
RACSubject *subD = [RACReplaySubject subject];

NSMutableArray *array2 = [NSMutableArray array];

//订阅信号
[[subC concat:subD] subscribeNext:^(id  _Nullable x) {
     [array2 addObject:x];
 }];

//发送信号
[subD sendNext:@"D"];
[subC sendNext:@"C"];
[subC sendCompleted];

//输出: [C, D]
NSLog(@"%@", array2);

2、then:用于连接两个信号,当第一个信号完成,才会连接then返回的信号

RACSubject *subjectA = [RACReplaySubject subject];
RACSubject *subjectB = [RACReplaySubject subject];

//发送信号
[subjectA sendNext:@"A"];
[subjectA sendCompleted];
[subjectB sendNext:@"B"];

//订阅信号
[[subjectA then:^RACSignal * _Nonnull{
     return subjectB;
}] subscribeNext:^(id  _Nullable x) {
     NSLog(@"%@", x);
}];

//这里只会输出: B  不会输出: A

3、merge:把多个信号合并为一个信号,任何一个信号有新值的时候就会调用

// 只要想无序的整合信号数据
RACSubject *subjectA = [RACSubject subject];
RACSubject *subjectB = [RACSubject subject];
RACSubject *subjectC = [RACSubject subject];

//合并信号
RACSignal *single = [[subjectA merge:subjectB] merge:subjectC];

//订阅信号
[single subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];

//发出消息
[subjectA sendNext:@"A"];
[subjectC sendNext:@"C"];
[subjectB sendNext:@"B"];
//输出结果(分别输出): A, C, B

4、zipWith:把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的next事件

// 只要想无序的整合信号数据
RACSubject *subjectA = [RACSubject subject];
RACSubject *subjectB = [RACSubject subject];

//合并信号
RACSignal *single = [subjectA zipWith:subjectB];

//订阅信号
[single subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];

//发出消息
[subjectA sendNext:@"A"];
[subjectB sendNext:@"B"];

//输出: 元祖(A, B)

5、combineLatest:将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号
6、reduce(聚合):用于信号发出的内容是元组,把信号发出元组的值聚合成一个值

@weakify(self)
RAC(self.loginButton, enabled) = [RACSignal combineLatest:@[self.phoneTextField.rac_textSignal,self.passwordTextField.rac_textSignal] reduce:^id _Nonnull(NSString * account, NSString * password){
    @strongify(self)
    if (account.length && (password.length > 5)) {
        self.loginButton.backgroundColor = [UIColor blueColor];
        return @(YES);
     }else{
        self.loginButton.backgroundColor = [UIColor lightGrayColor];
        return @(NO);
     }
}];

7、filter:过滤,每次信号发出,会先执行过滤条件判断

[[_accountText.rac_textSignal filter:^BOOL(NSString * _Nullable value) {
     //类似手机号的输入, 只有等于11位的时候才返回true
     return value.length == 11;
}] subscribeNext:^(NSString * _Nullable x) {
      //这里只会返回等于11位的字符
      NSLog(@"filter = %@", x);
}];

8、ignore:内部调用filter过滤,忽略掉ignore的值

//当88时,下面的log不输出
[[self.phoneTextField.rac_textSignal ignore:@"88"] subscribeNext:^(NSString * _Nullable x) {
     NSLog(@"ignore = %@", x);
}];

9、distinctUntilChanged:当上一次的值和当前的值有明显的变化就会发出信号,否则会被忽略掉。常用于UI刷新,即只有两次数据不一样才需要刷新UI。

//创建信号
RACSubject *subject = [RACSubject subject];

//订阅
[[subject distinctUntilChanged] subscribeNext:^(id  _Nullable x) {
     NSLog(@"distinctUntilChanged = %@", x);
}];

[subject sendNext:@12];
[subject sendNext:@12];
[subject sendNext:@23];

/*
  输出结果:只会输出两次
  distinctUntilChanged = 12
  distinctUntilChanged = 23
*/

10、take:从开始一共取N次的信号, 当遇到sendCompleted语句执行时, 会提前停止发送信号

RACSubject *subject1 = [RACSubject subject];
[[subject1 take:2] subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];

[subject1 sendNext:@1];
[subject1 sendNext:@2];
[subject1 sendNext:@3];

//输出: 1, 2

//如果上面发送信号的代码调整为
[subject1 sendNext:@1];
[subject1 sendCompleted];
[subject1 sendNext:@2];
[subject1 sendNext:@3];

//那么输出结果将会,只输出: 1

11、takeLast:取调用sendCompleted之前的N次信号,前提条件,订阅者必须调用sendCompleted,否则不会执行任何操作

RACSubject *subject1 = [RACSubject subject];
[[subject1 takeLast:2] subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];

[subject1 sendNext:@1];
[subject1 sendNext:@2];
[subject1 sendNext:@3];
[subject1 sendCompleted];

/*
输出:
  2
  3
*/

12、takeUntil:只要传入的信号发送完成或者subject2开始发送信号的时候,就不会再接收信号的内容

RACSubject *subject1 = [RACSubject subject];
RACSubject *subject2 = [RACSubject subject];

[[subject1 takeUntil:subject2] subscribeNext:^(id  _Nullable x) {
    NSLog(@"%@", x);
}];

[subject1 sendNext:@11];
[subject1 sendNext:@12];
//    [subject1 sendCompleted];
[subject1 sendNext:@13];
[subject2 sendNext:@"21"];
[subject2 sendNext:@"22"];

//输出: 11, 12, 13
//当sendCompleted取消注释的时候, 只会输出: 11, 12

13、skip:跳过N个信号后, 再开始订阅信号

//创建信号
RACSubject *subject = [RACSubject subject];

//订阅信号(要求跳过2个信号)
[[subject skip:2] subscribeNext:^(id  _Nullable x) {
     NSLog(@"%@", x);
}];

//发送信号
[subject sendNext:@1];
[subject sendNext:@2];
[subject sendNext:@3];
[subject sendNext:@4];

//因为上面跳过了两个信号, 所以这里只会输出: 3, 4

14、switchToLatest:用于signalOfSignals(信号的信号),有时候信号也会发出信号,会在signalOfSignals中,获取signalOfSignals发送的最新信号

//信号的信号
RACSubject *subject1 = [RACSubject subject];
RACSubject *subject2 = [RACSubject subject];

//获取信号中信号最近发出信号,订阅最近发出的信号
[[subject1 switchToLatest] subscribeNext:^(id  _Nullable x) {
     NSLog(@"%@", x);
}];

//subject1发送subject2信号
[subject1 sendNext:subject2];
[subject2 sendNext:@"信号中信号"];

//最终结果输出: "信号中信号"

15、timeout:超时, 可以让一个信号在一定时间后自动报错

//timeout: 超时, 可以让一个信号在一定时间后自动报错
RACSignal *single = [[RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
        return nil;
}] timeout:2 onScheduler:[RACScheduler currentScheduler]];

[single subscribeNext:^(id  _Nullable x) {
        NSLog(@"%@", x);
} error:^(NSError * _Nullable error) {
        //2秒后自动调用
        NSLog(@"%@", error);
}];

12、注意

1、系统要求:ReactiveCocoa 要求 OS X 10.8+ 以及 iOS 8.0+。

2、强引用问题

@weakify 和@strongify 这个两个宏是为了解决在使用block的时候强引用问题,注意这两个宏成对使用才有效。

@weakify(self)
[[self.forgotPasswordButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
     @strongify(self)
     //发送数据
     [self.forgotPasswordButtonRACSubject sendNext:nil];
}];
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容