RAC函数响应式编程

本文会有浅入深,介绍ReactiveCocoa为何深受iOS开发工程师的喜爱。以及简单的使用ReactiveCocoa完成一些基本的UI交互。
从最基本的开始,button有一些点击事件,我们通常是这样做的:

psb.png
   然后再moreButtonPressed中添加需要执行的操作。这样显然会把要监听的事件和处理的事件隔离开来。
   不是很方便我们找到相应的代码处理块。这是不符合高聚合,低耦合的编程思想的。
   那么高端一点的会怎样解决这个问题呢?下边是升级的版本,请观察:
psb-2.png

这样可以把要处理的事情,和监听的事情的代码放在一起。很容易就找到要处理的按钮的点击事件。
这是为了实现高聚合,低耦合的编程思想而封装的扩展类,基本都是block方法中处理点击,长按手势和滑动操作等事件。
那么下边就需要介绍,根据这种编程思想,衍生出来的ReactiveCocoa框架。它通过信号量和订阅者的方式,实现了交互逻辑。

2.ReactiveCocoa作用

 在我们iOS开发过程中,经常会响应某些事件来处理某些业务逻辑,例如按钮的点击,上下拉刷新,网络请求,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。
 
 其实这些事件,都可以通过RAC处理,ReactiveCocoa为事件提供了很多处理方法,而且利用RAC处理事件很方便,可以把要处理的事情,和监听的事情的代码放在一起,这样非常方便我们管理,就不需要跳到对应的方法里。非常符合我们开发中高聚合,低耦合的思想。
psb-4.png
 Signal信号是数据流,可以被绑定和传递。可以把信号想象成水龙头,只不过里 面不是水,而是玻璃球(value),直径跟水管的内径一样,这样就能保证玻璃球是依次排列,不会出现并排的情况(数据都是线性处理的,不会出现并发情况)。水龙头的开关默认是关的,除非有了接收方(subscriber),才会打开。
 这样只要有新的玻璃球进来,就会自动传送给接收方。接收方就是放在水龙头下的盆子,对于水龙头不同的出水状况,有自己的处理方式。水龙头出水时会通知下方的水盆,如果没有水盆,水龙头始终处于关闭状态。
 信号Signal是ReactiveCocoa核心部件之一。有信号选择器内置UIKit组件。例如,有一个rac_textsignal UITextField的值与在文本字段中的每一个新的按键发送。我们将在下一节中看到如何使用信号来执行工作。
 信号也可以被链接和转化。当我们映射或过滤一个流时,我们创建一个新的流。随后这流被映射,过滤,随意摆弄都没问题。我们会看到更多的关于下一章的链接。
 
 # 处理逻辑 · 数据内容
 #订阅
 处理逻辑指的是创建信号的时候,它是如何通知订阅者(Subscriber)并选择发送何种事件的。
 数据内容指的是信号会传递给订阅者(Subscriber)什么样的数据。这就像一个水龙头,它什么时候告诉水盆自己正在滴水,或是已经滴完水了。以及它把什么丢入水盆,是原始的水滴,还是水滴的质量?
 如果我们需要对这些内容进行自定义的修改,那么修改原信号显然是不可行的(信号已经被创建了)。因此,这就牵涉到信号之间的转换(Map)与组合(Combine)。
 我们从RACSignal类最基础的方法开始讨论信号之间的转换(Map)与组合(Combine)。对于每一个方法,我们需要关注这个方法的功能、返回值类型。由于ReactiveCocoa大量使用了block,还需要关注方法中block的参数类型和返回值类型。
 订阅是在数据流上进行的,通常是信号,当你想被通知时,一个新的值被发送(或下一个错误,或完成)。信号经常被用来产生一些其他的作用,例如下面的例子。
 让我们添加一个文本字段的视图控制器的视图,并将它连接到一个IBOutlet。我要使用内置的脚本和助理编辑这样做,但你做任何你喜欢做的事。
 
 #导出状态
 导出状态中的另一个核心部件ReactiveCocoa。我们可以将属性抽象成一个流,而不是在一个类中有一个属性设置为新的值。让我们以前一个例子,并用派生的状态来扩充它。
 让我们假装我们的观点是一种形式为创建一些帐户,我们只希望允许电子邮件地址,包含一个' @ '符号。当,只有当一个有效的用户名已输入,然后一个按钮的启用状态将是肯定的。我们还想提供反馈给用户的文本颜色的文本字段的方式。
 让我们首先添加按钮,创建一个IBOutlet。
psb-5.png
[self.textField.rac_textSignal subscribeNext:^(id x){
    NSLog(@"New value: %@", x);
    
   }error:^(NSError*error){
       
       NSLog(@"Error: %@", error);
   }completed:^{
       NSLog(@"Completed.");
}];

//当输入文本中不包含@字符的时候,是不允许用户创建账户的。这就是map的功能。
// RAC(self.button,enabled)=[self.textField.rac_textSignal map:^id(NSString *value){
// return @([value rangeOfString:@"@"].location != NSNotFound);
//  }];
/*
  RAC()宏需要两个参数:一个对象,该对象的一个关键路径。然后,它执行一个单向绑定的表达式的右手值的关键路径问题。值必须NSObjects,这就是NSNumber布尔表达式。
 但是文字颜色呢?我们实际上可以重新使用的信号从早期的一点点重构。
 */

RACSignal *validEmailSignal=[self.textField.rac_textSignal map:^id(NSString *value){
    return @([value rangeOfString:@"@"].location != NSNotFound);
}];
//    RAC(self.button,enabled)= validEmailSignal;
//    RAC(self.textField,textColor)=[validEmailSignal map:^id(id value){
//        if ([value boolValue]) {
//            return [UIColor orangeColor];
//        } else {
//     return [UIColor redColor]; }
//    }];
/*
 当您希望在响应于用户交互事件时发出的信号值时,命令是有用的。该命令的信号可以订阅到稍后处理我们返回的信号的输出。
 现在,替换在我们的按钮上启用的属性的绑定到以下。
 
按下按钮,信号块被执行时,和rac_command财产照顾绑定启用信号按钮的启用状态(事实上,如果我们把我们的原代码,我们将引起错误两绑定到同一个属性)。
 但是返回值是什么呢,我们需要返回一个信号将被发送到executionsignals管属于raccommand。这让您返回一个信号,表示一些工作,按结果更改按钮的相关操作。该按钮将继续被禁用,直到该信号返回其完整的值(立即返回此值为空)。因为我们只会记录下按钮的结果,所以在这种情况下,我们返回一个空信号。
 */
self.button.rac_command=[[RACCommand alloc] initWithEnabled:validEmailSignal signalBlock:^RACSignal *
 (id input){
     NSLog(@"Button was pressed.");
     return [RACSignal empty];
 }];

RACSignal

 RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    _subscriber = subscriber;
    //当订阅者被强引用,且没有被销毁,那么释放信号的方法就不会被执行
    [subscriber sendNext:@(1)];
    
    return [RACDisposable disposableWithBlock:^{
        NSLog(@"信号被取消");
    }];
    return  nil;
}];

RACDisposable *disposable =  [signal subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

//手动取消订阅,即使订阅者没有被销毁,仍然可以取消该信号
[disposable dispose];

RACSubject

 racsubject是一个有趣的信号类型。这是ReactiveCocoa创建的“可变状态“世界。这是一个信号,你可以手动发送新的值。因为这个原因,除了在特定的情况下,它是不推荐使用的。
 Hot and Cold Signals  热信号和冷信号
 信号在通常情况下是懒惰的,我们可以称之为懒加载。只有当他们被订阅的时候,才去发送信号和执行相应的操作。每添加一个新的订阅者,就需要重新去执行相应的操作。对于琐碎的操作来说,这是合理的也是可取的。我们称这类信号为热信号。
 然而,有时,我们希望工作立即完成。这种类型的信号被称为热信号。使用热信号是非常罕见的。
//既能创建信号,也能发送信号
RACSubject *subject = [RACSubject subject];

//创建订阅者
[subject subscribeNext:^(id x) {
    NSLog(@"订阅者1接收到数据:%@",x);
}];

[subject subscribeNext:^(id x) {
    NSLog(@"订阅者2接收到数据:%@",x);
}];

//发送数据  遍历所有的订阅者,然后发送数据
[subject sendNext:@3];

//不同信号的订阅方式不同,请谨慎

RACReplaySubject

// RACReplaySubject 可以先发送数据,然后订阅,不同于上边的信号类型
RACReplaySubject *replaySubject = [RACReplaySubject subject];

//订阅方式不一样,遍历所有的值,拿到当前的订阅者,去发送
[replaySubject subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

//sendNext :保存数据,遍历所有的订阅者,发送数据
[replaySubject sendNext:@(1)];


//RAC 集合类
NSArray *array = [NSArray arrayWithObjects:@"213",@"123",@"23333",@"212323", nil];
RACSequence *sequence =   array.rac_sequence;
RACSignal *arraySignal =  sequence.signal;
//订阅集合信号,内部会自动遍历所有元素
[arraySignal subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

[array.rac_sequence.signal subscribeNext:^(id x) {
     NSLog(@"%@",x);
    RACTupleUnpack(NSString *key,NSString *value) = x;
     NSLog(@"%@  %@",key,value);
}];


//1:代替代理 2:代理KVO  3: 代替监听事件 4: 代替通知   5:监听文本框文字变化   6: 处理界面有多次请求时候

Rac_liftSelector

 //这里重点讲解一下 当界面需要多个网络请求的时候,需要全部获取数据后显示界面时,才显示该界面。
//当数组中的数据都发送的时候,才调用该方法

RACSignal *hotSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"hot"];
    return nil;
}];

RACSignal *favouriteSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"favourite"];
    return nil;
}];


[self rac_liftSelector:@selector(reloadData:favourite:) withSignals:hotSignal,favouriteSignal, nil];

//传递的参数,即是我们信号发送的数据类型  [subscriber sendNext:@"favourite"];    [subscriber sendNext:@"hot"];
- (void)reloadData:(NSString *)hotString  favourite:(NSString *)favouriteString{
          NSLog(@"数据都请求完毕");
}

RACMulticastConnection

   多播
 多播是指在任何一个订阅用户数之间共享的一个术语。信号,默认情况下,是冷的(如我们所描述的最后一节)。当它被订阅时,不希望有一个寒冷的信号,执行工作的每一次操作。
 所以我们创造了从信号的racmulticastconnection。你可以通过使用公开发布racsignal,或组播方法。前一种方法为您创建一个多播连接。后者也是这样做。这个问题上是手动从底层发送信号时,它被称为发送的值。然后,而不是任何有兴趣的潜在信号发送的值订阅连接的信号

 //每次订阅不要都请求数据,只需要拿到数据就可以了,不管订阅多少次,都只请求一次 把信号转化为连接类,订阅连接类的信号

//把信号转化为连接类
RACMulticastConnection *multicastConnection =  [signal publish];

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

[multicastConnection.signal subscribeNext:^(id x) {
    NSLog(@"连接类2%@",x);
}];


[multicastConnection.signal subscribeNext:^(id x) {
    NSLog(@"连接类3%@",x);
}];
//连接
[multicastConnection connect];

RACCommend

    //RACCommend 处理事件,把事件数据都包装在累中处理,很方便的监听事件是否执行完成  不能返回一个空的信号
RACCommand *commend = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
    NSLog(@"%@",input); //只要执行命令就会调用该方法。
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        //发送数据
        [subscriber sendNext:@"执行命令产生的数据"];
        //发送完成
        [subscriber sendCompleted];
        return nil;
    }];
}];

//监听事件是否完成  当前命令内部发送数据完成,一定要主动发送完成
[commend.executing subscribeNext:^(id x) {
    if ([x boolValue]== YES) {
         NSLog(@"当前正在执行");
    }else{
        NSLog(@"执行完成/还没有执行");
    }
}];

//executionSignals:信号源,信号中的信号,发送的数据就是信号,而不是一些string,数组之类的,commend.executionSignals 必须在执行之前就订阅
[commend.executionSignals subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

//获取最新发送的信号,只能用于信号中的信号 switchToLatest 信号中的信号发送的最新数据
[commend.executionSignals.switchToLatest subscribeNext:^(id x) {
     NSLog(@"%@",x);
}];

//如何拿到执行命令中产生的数据  订阅命令内部的信号   @1 订阅信号   @订阅信号
RACSignal *commendSignal =  [commend execute:@(1)];

[commendSignal subscribeNext:^(id x) {
    NSLog(@"%@",x);
}];

RAC bind:^RACStreamBindBlock]

//RACBind信号
RACSignal *bindSignal = [subject bind:^RACStreamBindBlock{
    
    return ^RACSignal *(id value, BOOL *stop){
        //block调用:只要源信号发送数据,就会调用, 处理源信号数据
        NSLog(@"%@",value);
        value = [NSString stringWithFormat:@"MMM%@",value];
        //返回信号,不能为nil
        return [RACReturnSignal return:value];
    };
}];

[bindSignal subscribeNext:^(id x) {
    NSLog(@"%@",x);

}];

[subject sendNext:@(2)]; 

RAC flattenMap 绑定信号

RACSignal *flattenMapSignal =  [subject flattenMap:^RACStream *(id value) {
    //返回信号用来包装成修改的内容
    return [RACReturnSignal return:value];
}];

//绑定信号中返回什么信号,订阅的就是什么信号

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

}];
//发送数据
[subject sendNext:@"YES"];

RAC mapSignal 绑定信号

RACSignal *mapSignal = [subject map:^id(id value) {
    return @"YES";
}];

[mapSignal subscribeNext:^(id x) {
     NSLog(@"%@",x);
}];

[subject sendNext:@"YES"];

信号之间的组合 concat

 在开发中,经常用RACSignal 发送网络请求 
 信号之间的组合 concat     [subscriber sendCompleted];
 RACSignal *requestSignalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"网络请求1"];
    //请求完成 然后请求下边的网络请求
    [subscriber sendCompleted];
    return nil;
}];


RACSignal *requestSignalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"网络请求2"];
    return nil;
}];

RACSignal *concatSignal = [requestSignalA concat:requestSignalB];
[concatSignal subscribeNext:^(id x) {
     NSLog(@"%@",x);
}];

信号之间的组合 then

RACSignal *thenSignal = [requestSignalA then:^RACSignal *{
    return requestSignalB;
}];

[thenSignal subscribeNext:^(id x) {
    NSLog(@"%@",x);

}];

信号的超时,延迟,重试

RACSignal *signal = [self rac_signalForSelector:@selector(commandAction:)];
    [[[RACSignal interval:1 onScheduler:[RACScheduler scheduler]]takeUntil:signal] subscribeNext:^(id x) {
        NSLog(@"%@",[NSDate date]);
    }];
    
    RACSignal *delaySignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"2"];
        return [RACDisposable disposableWithBlock:^{
        }];
    }];
    
    [[delaySignal delay:2] subscribeNext:^(id x) {
        NSLog(@">>>>>>%@",[NSDate date]);
        [self commandAction:nil];
    }];
    
    [[delaySignal retry] subscribeNext:^(id x) {
        NSLog(@"%@",x);
    } error:^(NSError *error) {
        NSLog(@"%@",error);
    }];

psb-6.png

psb-7.png

Functional Reactive Programming 文档: http://pan.baidu.com/s/1o61mB4e
其他一些关于ReactiveCocoa的博客地址: http://www.mamicode.com/info-detail-661152.html
相关唐巧:http://blog.devtang.com/blog/2016/01/03/reactive-cocoa-discussion/
探究RAC-RAC信号处理方法归纳 :http://cbsfly.github.io/ios/rac2/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

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

推荐阅读更多精彩内容