iOS开发之ReactiveCocoa的基本用法干货分享

一、概述

ReactiveCocoa简称RAC,基于响应式编程思想的Objective-C实践。本文主要结合代码示例列举了RAC的常用类,常用方法和常用宏,帮助开发者快速上手怎样使用RAC。

二、常用的类

1、RACSiganl:信号类,用于传递改变的数据,可传递以下三种状态:
  • sendNext:可理解为传递正确数据,告诉订阅者进行下一步处理
  • sendError:传递的数据错误,告诉订阅者错误处理
  • sendCompleted:告诉订阅者已完成
2、RACSubscriber:表示订阅者的意思,用于发送信号,这是一个协议,不是一个类,只要遵守这个协议,并且实现方法才能成为订阅者。通过create创建的信号,都有一个订阅者,帮助他发送数据。
  • 没有订阅者的 RACSiganl 就是冷信号,相反有订阅者的 RACSiganl 就是热信号。为了更好地理解,请参考下图:


    RAC.png

下面举个RAC简单用法的栗子:

   //信号
   RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
      [subscriber sendNext:@"666666"]; //发送正确数据
//      [subscriber sendError:[NSError errorWithDomain:@"xxx" code:1001 userInfo:@{@"errorMessage" : @"错误"}]]; //发送错误
//      [subscriber sendCompleted]; //完成
      return nil;
   }];
   //订阅者
   [signal subscribeNext:^(id  _Nullable x) {
      NSLog(@"--%@--",x); //接收到数据
   } error:^(NSError * _Nullable error) {
      NSLog(@"--%@--",error); //出错处理
   } completed:^{
      NSLog(@"完成"); // 信号完成
   }];

createSignal 方法block中对应的就是上面说的 RACSiganl 传递的三种状态(sendNext, sendError, sendCompleted),subscribeNext就是订阅方法,对应着 RACSiganl 发出的三种状态做出不同的处理。

3、RACSubject:既可以充当信号,也可以发送信号

其实用法和上面栗子类似:

   RACSubject *subj = [RACSubject subject];
   //订阅信号
   [subj subscribeNext:^(id  _Nullable x) {
      NSLog(@"--%@--",x); //接收到数据
   } error:^(NSError * _Nullable error) {
      NSLog(@"--%@--",error); //出错处理
   } completed:^{
      NSLog(@"完成"); // 信号完成
   }];
   //发送信号
   [subj sendNext:@"666666"]; //发送正确数据
   //      [subscriber sendError:[NSError errorWithDomain:@"xxx" code:1001 userInfo:@{@"errorMessage" : @"错误"}]]; //发送错误
   //      [subscriber sendCompleted]; //完成

RACSubject的作用通常用来代替代理。下面上代码:

  • 首先创建一个RedView的类继承于UIView

RedView.h

#import <UIKit/UIKit.h>
#import <ReactiveObjC/ReactiveObjC.h>

@interface RedView : UIView
@property (nonatomic,strong) RACSubject *subject;
@end

RedView.m

#import "RedView.h"
@implementation RedView

- (RACSubject *)subject {
    if (_subject ==  nil) {
        _subject = [RACSubject subject];
    }
    return _subject;
}

- (IBAction)btnClick:(UIButton *)sender {
    [self.subject sendNext:@"777777"];
}
@end

控制器中代码:

#import "OrderController.h"
#import <ReactiveObjC/ReactiveObjC.h>
#import "RedView.h"

@interface OrderController ()
@property (weak, nonatomic) IBOutlet RedView *redView;

@end

@implementation OrderController

- (void)viewDidLoad {
   [super viewDidLoad];
   
   [self.redView.subject subscribeNext:^(id  _Nullable x) {
      NSLog(@"--%@--",x); //接收到数据
   }];
}

这样就完成了和代理一样的功能

4、RACMulticastConnection:信号连接类

一般情况下,信号被订阅多少次,信号创建是的block就调用多少次。但是实际开发中,block只需调用一次就可以了。RACMulticastConnection可以实现不管订阅多少次信号,信号的block只请求一次:

- (void)viewDidLoad {
   [super viewDidLoad];
   //不管订阅多少次信号,只请求一次
   //1.创建信号
   RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
      NSLog(@"请求数据");
      [subscriber sendNext:@"请求数据:777"];
      return nil;
   }];
   //2.把信号转换成连接类
   RACMulticastConnection * connection = [signal 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];
}

上面的例子中信号被订阅了3次,但是请求数据只请求了1次。

5、RACCommand:处理事件的类
- (void)viewDidLoad {
   [super viewDidLoad];
   ///1.创建命令
   RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id  _Nullable input) {
      
      /*******此block执行命令时调用*******/
      //input:对应执行命令的参数,拿到参数后可以写处理事件代码
      NSLog(@"input:%@",input);
      //不能返回空的信号
      return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
         //传递事件产生的数据
         [subscriber sendNext:@"执行命令产生的数据"];
         return nil;
      }];
   }];
   
   ///2.订阅命令内部的信号
   //方式1:
   //1.执行命令
   RACSignal *signal = [command execute:@"666"];
   //2.订阅信号
   [signal subscribeNext:^(id  _Nullable x) {
      NSLog(@"~%@~",x);
   }];
   //方式2:(注意:必须在执行命令前订阅信号)
   //1.订阅信号
   //executionSignals:信号源,信号中的信号
   [command.executionSignals subscribeNext:^(id  _Nullable x) {
      [x subscribeNext:^(id  _Nullable x) {
         NSLog(@"~%@~",x);
      }];
   }];
   //2.执行命令
   [command execute:@"777"];

   ///3.监听事件有没有完成
   [command.executing subscribeNext:^(NSNumber * _Nullable x) {
      if ([x boolValue] == YES) {
         NSLog(@"正在执行");
      }else {
         NSLog(@"执行完成/没有执行");
      }
   }];
}

上面的例子例举了创建command对象block中,事件如何处理,事件产生的数据如何传递。command.executing监控事件执行的过程。

三、常用的方法

  • 监听某个对象是否调用某个方法:
#import "OrderController.h"
#import <ReactiveObjC/ReactiveObjC.h>
#import "RedView.h"
#import "HomeController.h"

@interface OrderController ()
@property (weak, nonatomic) IBOutlet RedView *redView;

@end

@implementation OrderController
- (void)viewDidLoad {
   [super viewDidLoad];
   
   [[self rac_signalForSelector:@selector(push:)] subscribeNext:^(RACTuple * _Nullable x) {
      NSLog(@"跳转");
   }];
}
- (IBAction)push:(id)sender {
   HomeController *vc = [[HomeController alloc] init];
   [self.navigationController pushViewController:vc animated:YES];
}

当点击按钮调用 push: 方法时就会监听到 push: 方法被调用了,从而打印出 “跳转”

  • 代替KVO:
#import "OrderController.h"
#import <ReactiveObjC/ReactiveObjC.h>
#import "RedView.h"

@interface OrderController ()
@property (weak, nonatomic) IBOutlet RedView *redView;

@end

@implementation OrderController
- (void)viewDidLoad {
   [super viewDidLoad];
   [[_redView rac_valuesForKeyPath:@"frame" observer:self] subscribeNext:^(id  _Nullable x) {
      //修改的值
      NSLog(@"%@",x);
   }];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
   _redView.frame = CGRectMake(50, 50, 200, 200);
}

上述例子同KVO功能一样,监听了_redView对象对象中的frame属性,当点击屏幕调用touchesBegan改变_redView的frame时,block里的方法会被调用。

  • 监听事件
[[_btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
      NSLog(@"按钮被点击了");
   }];

给按钮添加点击事件

  • 代替通知
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillShowNotification object:nil] subscribeNext:^(NSNotification * _Nullable x) {
      NSLog(@"键盘被弹出");
   }];

实现监听键盘弹出通知

  • 监听文本框变化
[[self.textField rac_textSignal] subscribeNext:^(NSString * _Nullable x) {
      NSLog(@"UITextField:%@",x);
   }];
  • 所有信号都返回结果的时候,统一做处理
- (void)viewDidLoad {
   [super viewDidLoad];
   //信号一
   RACSignal *sg1 = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
      //模拟从服务器请求数据
      [subscriber sendNext:@"请求的数据1"]; //请求成功后发送数据
      return nil;
   }];
   //信号二
   RACSignal *sg2 = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber>  _Nonnull subscriber) {
      //模拟从服务器请求数据
      [subscriber sendNext:@"请求的数据2"]; //请求成功后发送数据
      return nil;
   }];
   //方法upDateUI的参数,对应每个信号发送的数据
   [self rac_liftSelector:@selector(upDateUIWithData1:data2:) withSignalsFromArray:@[sg1,sg2]];
}
- (void)upDateUIWithData1:(NSString *)data1 data2:(NSString *)data2{
   //拿到请求数据
   NSLog(@"更新UI:%@--%@",data1,data2);
}

四、常用的宏

  • RAC(xx,xx):给某个对象的某个属性绑定信号,只要产生信号内容,就会把内容给属性赋值
@interface OrderController ()
@property (weak, nonatomic) IBOutlet UITextField *textField;
@property (weak, nonatomic) IBOutlet UILabel *label;
@end

@implementation OrderController

- (void)viewDidLoad {
   [super viewDidLoad];
   //方法一,普通方法实现
   @weakify(self)
   [_textField.rac_textSignal subscribeNext:^(NSString * _Nullable x) {
      @strongify(self)
      self.label.text = x;
   }];
   //方法二,宏方法实现
   RAC(_label,text) = _textField.rac_textSignal;
}

上面例子中方法二是利用宏实现的,首先将_label的text属性绑定了_textField的信号,只要_textField内容发生改变,就会赋值给_label的text属性。 例子中两种方法实现的效果一样,用RAC()宏写更简便。

  • RACObserve(_redView, frame):只要对象属性改变就会产生信号
@interface OrderController ()
@property (weak, nonatomic) IBOutlet RedView *redView;
@end

@implementation OrderController
- (void)viewDidLoad {
   [super viewDidLoad];
   [RACObserve(_redView, frame) subscribeNext:^(id  _Nullable x) {
      //修改的致
      NSLog(@"%@",x);
   }];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
   _redView.frame = CGRectMake(50, 50, 200, 200);
}

与上面说的代 替KVO: 举的例子实现效果一样

  • @weakify(self),@strongify(self):解决RAC中的循环引用
 @weakify(self)
   [_textField.rac_textSignal subscribeNext:^(NSString * _Nullable x) {
      @strongify(self)
      self.label.text = x;
   }];

在block外加@weakify(self)方法,block里面加 @strongify(self)方法,之后只用self.就不会产生循环引用。

五、RAC高级操作方法

  • bind
    具体操作:给RAC的信号进行绑定,只要信号已发送数据,就能监听到,从而把数据改成自己想要的
- (void)viewDidLoad {
   [super viewDidLoad];
   
   //1.创建信号
   RACSubject *subj = [RACSubject subject];
   //2.绑定信号
   RACSignal *bindSignal = [subj bind:^RACSignalBindBlock _Nonnull{
      
      return ^RACSignal *(id value, BOOL *stop) {
         //只要源信号发送数据,就会调用此block
         //此block作用:处理源信号内容
         //value:信号源发送内容
         value = [NSString stringWithFormat:@"orange%@",value];
         NSLog(@"修改后的value:%@",value);
         return [RACReturnSignal return:value];
      };
   }];
   //3.订阅绑定信号
   [bindSignal subscribeNext:^(id  _Nullable x) {
      NSLog(@"接收到绑定信号处理完的信号:%@",x);
   }];
   //4.发送数据
   [subj sendNext:@"888888"];
}

上述例子中,发送的信号为“888888”,通过绑定信号,对源信号数据进行修改,订阅后的信号内容就被修改了。

  • 映射
    映射可以把源信号内容更改成新的内容
- (void)viewDidLoad {
   [super viewDidLoad];
   //创建信号
   RACSubject *subj = [RACSubject subject];
   //绑定信号
   RACSignal *bindSignalX = [subj map:^id _Nullable(id  _Nullable value) {
      //返回的类型就是你需要映射的值
      return [NSString stringWithFormat:@"%d",[value intValue] + 1];
   }];
   //订阅信号
   [bindSignalX subscribeNext:^(id  _Nullable x) {
      NSLog(@"%@",x);
   }];
   //发送数据
   [subj sendNext:@"666666"];
}

上述例子中,绑定信号block中,将value值+1,直接return的值就是要映射的值,所以在订阅信号中的内容就在发送数据基础上+1。

  • 压缩信号:所有信号都发送内容时,才会调用
    使用场景:当一个界面多个请求时,要等所有请求完成更新UI
- (void)viewDidLoad {
   [super viewDidLoad];
   RACSubject *s1 = [RACSubject subject];
   RACSubject *s2 = [RACSubject subject];
   //压缩成一个信号
   RACSignal *yaS = [s1 zipWith:s2];
   //订阅信号
   [yaS subscribeNext:^(id  _Nullable x) {
      NSLog(@"%@",x);
   }];
   //发送信号
   [s1 sendNext:@"1"];
   [s2 sendNext:@"2"];
}

上面例子中,当s1和s2同时发送信号时,订阅信号的block才会调用。

  • 组合信号:将两个信号组合,并且可以同时处理两个信号的内容
    使用场景:登录页面账号输入框和密码输入框同时输入内容时,登录按钮才能点击
@interface HomeController ()
@property (weak, nonatomic) IBOutlet UIButton *btn;
@property (weak, nonatomic) IBOutlet UITextField *textField;
@property (weak, nonatomic) IBOutlet UITextField *textField1;
@end

@implementation HomeController

- (void)viewDidLoad {
    [super viewDidLoad];
    @weakify(self)
    //reduce:聚合的意思,合并两个信号数据,进行汇总计算时使用的
    RACSignal *signal = [RACSignal combineLatest:@[self.textField.rac_textSignal, self.textField1.rac_textSignal] reduce:^id (NSString *text, NSString *text1){
        //判断输入框位数(需转成NSNumber类型)
        BOOL isHidden;
        if (text.length >= 3 && text1.length >= 6) {
            isHidden = YES;
        }else {
            isHidden = NO;
        }
        return @(isHidden);
    }];
    //订阅信号
    [signal subscribeNext:^(id  _Nullable x) {
        NSLog(@"达成条件返回结果:%@",x);
        @strongify(self)
        self.btn.hidden = ![x boolValue];
    }];
}

上面例子中,combineLatest:方法组合了textField和textField1的信号,reduce的block中是对两个组合信号的数据进行计算并返回计算后的结果。通过订阅组合后的信号,就能拿到reduce处理后的解果,来判断登录按钮是否显示。

  • 过滤
    例1:过滤掉一些内容
[[_textField.rac_textSignal filter:^BOOL(NSString * _Nullable value) {
      //value源信号内容
      //返回值为过滤的条件
      return [value length] > 5 ; //只有当文本框内容大于5才能获取文本框内容
   }] subscribeNext:^(NSString * _Nullable x) {
      NSLog(@"大于5时打印内容:%@",x);
   }];

第一个block中直接返回想要过滤的条件,只有条件达成时,订阅信号subscribeNext的block内的方法才执行。

例2:忽略某个值

   RACSubject *igSub = [RACSubject subject];
   RACSignal *igSignal = [igSub ignore:@"1"];
   [igSignal subscribeNext:^(id  _Nullable x) {
      NSLog(@"%@",x);
   }];
   [igSub sendNext:@"1"];

利用ignore方法设置要忽略的内容:“1”,当发送的消息(sendNext)为“1”时,订阅消息时就接收不到了。

例3:当前值和上一个值相同就不会被订阅
使用场景:当模型里一个属性没有变就不会更新UI界面,只有不同时才会刷新UI

   RACSubject *disSub = [RACSubject subject];
   [[disSub distinctUntilChanged] subscribeNext:^(id  _Nullable x) {
      NSLog(@"%@",x);
   }];
   [disSub sendNext:@"1"];
   [disSub sendNext:@"1"];

两次发送的都是“1”,但是block中只执行了一次。

  • 标记
    takeUntil需要一个信号作为标记,当标记的信号发送数据,就停止。
   RACSubject * subject = [RACSubject subject];
   RACSubject * subject1 = [RACSubject subject];
   
   [[subject takeUntil:subject1] subscribeNext:^(id  _Nullable x) {
      NSLog(@"---%@---",x);
   }];
   [subject1 subscribeNext:^(id  _Nullable x) {
      NSLog(@"%@",x);
   }];
   
   [subject sendNext:@"1"];
   [subject sendNext:@"2"];
   [subject sendNext:@"3"];
   
   [subject1 sendNext:@"Stop"];
   
   [subject sendNext:@"4"];
   [subject sendNext:@"5"];

上面例子中subject1信号发送数据之后,subject就接收不到4和5了。

  • rac_willDeallocSignal
    对象销毁时发动的信号
// 监听文本框的改变,直到当前对象被销毁
[_textField.rac_textSignal takeUntil:self.rac_willDeallocSignal];

以上例子中,当前对象被销毁时,会发送rac_willDeallocSignal信号,rac_textSignal就停止监听了。

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