ReactiveCocoa------ReactiveCocoa操作详解

引言

在之前的一篇Post中对函数响应式编程和RAC做了简单剖析,强烈建议没看过的同学猛戳链接。

ReactiveCocoa的核心是RACSignal类代表的信号。信号会产生一个事件流,事件有三种类型next、completed、error,一个信号源可以发送任意数量的next时间,直到该信号结束(completed)或因错误(error)而关闭。通过对信号的订阅可以获得不同事件的回调。使用这个简单的事件流模型,通过对信号的变换、组合操作,我们可以处理日常iOS开发中的按钮点击、网络请求响应、KVO或用户位置变化等各种事件类型。

本篇我们会围绕RACSignal的基本使用及各类操作做详细探究。

RACSignal的创建

RAC中我们可以通过以下几种方式获得一个信号。

单元信号

1
2
3
4
RACSignal * signal1 = [RACSignal return:@"some Value"];
RACSignal * signal2 = [RACSignal error:[NSError new]];
RACSignal * signal3 = [RACSignal empty];
RACSignal * signal4 = [RACSignal never];

动态信号

1
2
3
4
5
6
7
8
RACSignal * signal5 = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendNext:@2];
[subscriber sendError:[NSError new]];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
}];
}];

Cocoa桥接

1
2
3
4
5
UIButton  * btn = [UIButton new];
RACSignal * signal6 = [btn rac_signalForSelector:@selector(setFrame:)];
RACSignal * signal7 = [btn rac_signalForControlEvents:(UIControlEventTouchUpInside)];
RACSignal * signal8 = [btn rac_willDeallocSignal];
RACSignal * signal9 = RACObserve(btn, backgroundColor);

信号变换

1
2
3
RACSignal * signal10 = [signal1 map:^id(NSString * value) {
return [value substringFromIndex:1];
}];

序列转换

1
2
RACSequence * sequence = [RACSequence return:@1];
RACSignal * signal11 = sequence.signal;

信号的订阅方式

订阅方法

RAC的作者煞费苦心的为我们提供了对一个信号的next、error、completed三种事件的排列组合的订阅方法,这里列举一种。

1
2
3
4
5
6
7
[signal11 subscribeNext:^(id x) {
NSLog(@"next value is %@",x);
} error:^(NSError *error) {
NSLog(@"Ops! Get some error:%@",error);
} completed:^{
NSLog(@"It finished success");
}];

绑定

1
RAC(btn, backgroundColor)  = [RACSignal return:[UIColor redColor]];

Cocoa桥接

1
[btn rac_liftSelector:@selector(convertRect:toView:) withSignals:signal1, signal2,nil];

RACSignal各类操作

引用一张上一篇Post中的图例

本篇文章主要围绕对值操作、对数量操作、对时间间隔操作及多个信号间的组合变换做深入探究,对维度的操作的探究会放在后续的文章中。

在深入下去之前,需要先了解一下RACTuple,RAC中很多操作都会用到或产生RACTuple。

RACTuple

RACTuple(元组类)是RAC中的集合类,其底层实现是对NSArray的封装。RACTuple有以下特点:

  • 遵循NSFastEnumeration协议(可用for in枚举)
  • 遵循NSCopy协议
  • 遵循NSCoding协议
  • 实现了objectAtIndexedSubscript:方法(可用下标访问元素)
  • 可把NSNull.null转为RACTupleNil.tupleNil
  • 封装了一系列遍历的集合操作方法

RACTuple的简单使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
//普通创建 
RACTuple *tuple1 = [RACTuple tupleWithObjects:@1, @2, @3, nil];
RACTuple *tuple2 = [RACTuple tupleWithObjectsFromArray:@[@1, @2, @3]];
RACTuple *tuple3 = [[RACTuple alloc] init];

//宏创建
RACTuple *tuple4 = RACTuplePack(@1, @2, @3, @4);

//解包(等号前面是参数定义,后面是已存在的Tuple,参数个数需要跟Tuple元素相同)
RACTupleUnpack(NSNumber * value1, NSNumber * value2, NSNumber * value3, NSNumber * value4) = tuple4;

//元素访问方式
NSLog(@"%@", [tuple4 objectAtIndex:1]); NSLog(@"%@", tuple4[1]);

RACSignal的操作

对于信号操作、变化后得到的结果会从Next事件、Error事件、Completed事件三个角度并结合相应图例给出,不会对其底层实现做深入探究。

对值操作

Map

Next事件:新信号的值由原信号值经过映射得出。(映射由传入block给出)
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

MapReplace

Next事件:原信号的值会被转换成一串相同的值。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

ReduceEach

ReduceEach操作要求原信号的值必须是一个元组(RACTuple)。

Next事件:对原信号发送的元组进行解包,利用解包后的值作为block的入参经过映射得出新的值。(映射由block给出)。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

ReduceApply

ReduceApply其功能类似于ReduceEach。不过这里和ReduceEach不同的是,源信号产生的每个元组(每个值)的第0个元素必须是一个block,后面n个元素为block的入参,第0位的block有几个入参,后面就需要有几个元素,多余的元素为无效元素。

Next事件:对原信号发送的元组进行解包,以元组的第0个元素为block,其余元素为入参得出新的值。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

Coding

不是很好理解,这里给出相应代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RACSignal * signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

id block = ^id(NSNumber * first,NSNumber * second){
return @(first.integerValue + second.integerValue);
};

[subscriber sendNext:RACTuplePack(block,@1,@2,@5)];
return [RACDisposable disposableWithBlock:^{
}];
}];

RACSignal * signalB = [signalA reduceApply];

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

上例代码中block只有两个入参,所以只会取1,2计算得出新值3,tuple内的元素5为无效元素。

Not

not操作要求源信号产生的每个值都是NSNumber类型。新信号的值由原信号值的布尔值取非得出。

Next事件:由原信号值的布尔值取非得出新的值。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

And

and操作要求源信号产生的每个值都是元组类型且元组内的每一个元素都必须是NSNumber类型。and操作会创建一个新的信号,新信号的值由元组内各元素的布尔值求与得出。

Next事件:由源信号发送的元组内各元素的布尔值求与得出新的值。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

Or

or操作与and操作类似,不同的是新信号的值由元组内各元素的布尔值求或得出。这里不做展开讨论。

Materialize

materialize操作会创建一个新信号,新信号的值为将原信号的值包装为RACEvent类型。

Next事件:由原信号的值包装为RACEvent类型得出新的值。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

源码

materialize操作通过源码更容易理解。通过对原信号的订阅,将原信号的值、error、complete包装成RACEvent类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (RACSignal *)materialize {
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
return [self subscribeNext:^(id x) {
[subscriber sendNext:[RACEvent eventWithValue:x]];
} error:^(NSError *error) {
[subscriber sendNext:[RACEvent eventWithError:error]];
[subscriber sendCompleted];
} completed:^{
[subscriber sendNext:RACEvent.completedEvent];
[subscriber sendCompleted];
}];
}] setNameWithFormat:@"[%@] -materialize", self.name];
}
Dematerialize

Dematerialize是materialize的逆操作,新信号的值为将原信号值(RACEvent类型)还原为正常的值信号。

Next事件:由原信号的值(RACEvent类型)还原得出新的值。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

源码

同样通过源码来看dematerialize操作。通过bind函数对原信号进行变换。新信号会根据event.eventType进行转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (RACSignal *)dematerialize {
return [[self bind:^{
return ^(RACEvent *event, BOOL *stop) {
switch (event.eventType) {
case RACEventTypeCompleted:
*stop = YES;
return [RACSignal empty];

case RACEventTypeError:
*stop = YES;
return [RACSignal error:event.error];

case RACEventTypeNext:
return [RACSignal return:event.value];
}
};
}] setNameWithFormat:@"[%@] -dematerialize", self.name];
}

数量操作

Filter

将原信号的值进行过滤后,符合条件的值会做为新信号的值返回,否则原信号的值会被吞掉。

Next事件:对原信号发送的值进行过滤后得出。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

Ignore

ignore的底层实现是对Filter的封装。对原信号的值进行校验,与传入值相等的值会被吞掉,其他值会作为新信号的值返回。这里的相等为满足 “==” 操作符 或 “isEqual” 操作符。

Next事件:对原信号发送的值进行过滤后得出。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

IgnoreValues

ignoreValues的底层实现同样是对Filter的封装。创建一个忽略所有原有信号值的信号。即新信号不会发送任何next事件。这里不做展开讨论。

DistinctUntilChanged

distinctUntilChanged操作会对接收到的原信号的值与原信号上一次发送的值做校验,如果相等则忽略该值,只有和原信号上一次发送的值不同才会做为新信号的值进行传递。这里的相等同样是指满足 “==” 操作符 或 “isEqual” 操作符。

Next事件:对原信号的值与上一次的值做校验,当前值与上一次值不一致时做为新信号的值发送。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

Take

take操作的入参为取原信号的前X个值,做为新信号的值返回,忽略原信号前x值以后的值。

Next事件:取原信号的前x个值作为新信号的值,忽略其他值。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

Skip

skip操作和take操作是补集关系。take是取原信号的前count个信号值,而skip是从原信号的第count + 1个信号值开始取值做为新信号的值返回。这里不做展开讨论。

TakeLast

takeLast和take用法一样,不过他取的是原信号的最后x个值。需要注意的是:takeLast一定要调用sendCompleted,告诉他发送完成了,这样才能取到最后几个值,这里也就不对其进行展开了。

TakeUntilBlock

根据传入的block做为校验条件。新信号透传原信号发送的值直至原信号发送的值满足校验条件,此时新信号停止发送值。

Next事件:取原信号的值作为新信号的值直至满足校验条件。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

SkipUntilBlock

根据传入的block做为校验条件。新信号忽略原信号发送的值直至原信号发送的值满足校验条件,此时新信号开始透传原信号发送的值。

Next事件:忽略原信号发送的值直至原信号发送的值满足校验条件。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

SkipWhileBlock

根据传入的block做为校验条件。新信号忽略原信号发送的值直至发送的值不满足校验条件。此时新信号开始透传原信号发送的值。

Next事件:忽略原信号发送的值直至原信号发送的值不满足校验条件。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

Any:

Any:操作根据传入block作为校验条件。如果原信号发送的值中有任何满足校验条件的值新信号发送@1(yes)。 如原信号中无任何满足校验条件的值,在原信号调用sendCompleted后,新信号发送@0(no)。
注意:使用Any:操作原信号必须调用sendCompleted,否则在所有值都校验失败的情况下新信号不会发送@0(no).

Next事件:原信号所发送的值中有任何值满足校验条件发送@1(yes),否则发送 @0(no)。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

校验失败

校验成功

Any

当原信号发送任何值,新信号就会发送@1(yes)。Any操作也需要原信号调用sendCompleted,否则在原信号不发送任何值的情况下,新信号不会发送@0(no),这里就不做展开了。

All:

All:操作以传入blcok作为校验条件,当原信号发送的任何值都满足校验条件时,新信号会发送@1(yes),否则新信号发送@0(no)。All:同样需要原信号调用sendCompleted。比较容易理解,也就不再展开。

StartWith

startWIth操作,利用contact(后续会提到)在透传原信号发送的值之前插入一个初始值。

Next事件:在原信号发送的值之前插入一个初始值。
Error事件:透传原信号error事件。
Completed事件:透传原信号completed事件。

图例

Repeat

在原信号发送completed事件后,持续重复发送原信号的值。

Next事件:在原信号发送completed之前,发送原信号的值。 在原信号发送completed事件后,持续重复发送原信号的值。
Error事件:透传原信号error事件。不再重复发送原信号的值。
Completed事件:无completed事件。

图例

原信号未发送error、completed

原信号发送error

原信号发送completed

Retry:

透传原信号发送的值,在原信号出现error的时候,重试x次(重复订阅原信号x次),如果依旧error那么就会停止重试。

Next事件: 发送原信号的值,在原信号发送error后,重试x次。
Error事件:在原信号发送error后,重试x次。如果依旧错误传递error。
Completed事件:透传completed事件。

图例

Retry

retry为无限重试操作。底层为调用retry:实现,此时的入参为0。这里不做展开讨论。

Collect

Next事件: 将原信号的值收集起来,保存在NSMutableArray中,做为新信号的值。在原信号发送completed事件后发送该值。
Error事件:在原信号发送error后,丢弃所有值并传递error事件。
Completed事件:收到原信号的completed事件后发送收集起来的值并透传completed事件。

图例

ScanWithStart:reduce:

Next事件: 以第一个入参为初始值,原信号每发送一个值以reduceBlock的策略为递推计算结果,计算结果作为新信号的值立刻发送。
Error事件:透传error事件。
Completed事件:透传completed事件。

图例

AggregateWithStart:reduce:

本质为调用scanWithStart:reduce:操作和takeLast组合实现。

Next事件: 以第一个入参为初始值,原信号每发送一个值以reduceBlock的策略为递推计算结果,在原信号发送sendCompleted后,新信号发送递推所得计算结果。
Error事件:透传error事件、丢弃计算结果。
Completed事件:发送计算结果并透传completed事件。

图例

AggregateWithStartFactory: reduce:

startFactory :aggregateWithStartFactory 本质为调用 aggregateWithStart实现,startFactory代码块的计算结果会作为start的入参传入aggregateWithStart。这里不做进一步展开。

AggregateWithStart:reduceWithIndex:

在aggregateWithStart:reduce: 的基础上,每次reduceBlock的递推都会带上统计当前递推次数的index。底层实现aggregateWithStart:reduce:为调用aggregateWithStart:reduceWithIndex:时忽略index参数。不做展开讨论。

ScanWithStart:reduceWithIndex:

功能上与scanWithStart:reduce:类似,同样在reduceBlock会携带当前递推次数的index。scanWithStart:reduce:的底层实现同样为调用scanWithStart:reduceWithIndex:时忽略block内的index入参。同样不做展开讨论。

对时间间隔操作

Delay:

Delay:操作对原信号的next事件和completed事件延时发送,比较容易理解这里不做展开讨论。
Next事件: 对原信号的值延迟x秒发送。
Error事件:立即透传error事件。
Completed事件:对于原信号的Completed事件延迟x秒发送。

Throttle:

throttle:通常用于搜索输入框,在用户享受实时更新搜索数据的同时减轻服务端压力。这里简单的Coding不能很形象说明,就不再编写代码。

Next事件: 原信号在x秒内无新的值产生,新信号发送该值。
Error事件:透传error事件,如果之前有未发送next事件丢弃该事件(不再发送)。
Completed事件:新信号立刻透传该事件,如果之前有未发送next事件的值立即发送该值。

图例

Throttle:valuesPassingTest:

在throttle:的基础上加入校验操作。

Next事件: 对原信号发送的值进行校验,校验失败丢弃该值。校验成功,若原信号在x秒内无新的校验成功的next值产生,新信号发送该值。
Error事件:透传error事件,如果之前有未发送next事件的值丢弃该值(不再发送)。
Completed事件:新信号立刻透传该事件,如果之前有未发送校验成功的next事件的值立即发送该值。

BufferWithTime:onScheduler

Next事件: 缓冲原信号未来一段时间内发送的next事件,将所有值包装成RACTuple返回。
Error事件:透传error事件,丢弃缓冲池内的值。
Completed事件:将缓冲池内的next事件立即发送后,立即透传completed事件。

多信号的组合、变换

Concat:

对A、B两个信号进行链接,A发送completed事件后开始发送B信号的事件。

Next事件:透传A信号的值,A信号发送completed事件后开始透传B信号的值。
Error事件:A、B任一信号发出error事件后透传该事件,且不再进行拼接操作。
Completed事件:B信号发送completed事件后,新信号发送completed事件。

图例

concat操作不会改变原有信号值的发送线程。

Merge:

将A、B两个信号进行合并。

Next事件:按时间顺序透传A、B两信号的next事件。
Error事件:A、B两信号,任意信号发送error事件,透传error事件。
Completed事件:A、B两信号,均发送completed事件后,新信号发送completed事件。

图例

merge操作不会改变原有信号值的发送线程。

Zip:

将A、B两信号发送的值压缩为一个元组。每个流的第一个值将被合并、然后是第二个、第三个,直到一个流被终止。

Next事件:在A、B两信号都发送next事件后,将next事件携带值合并成一个元组作。新信号以该元组为值发送next事件。
Error事件:A、B两信号,任意信号发送error事件,透传error事件。
Completed事件:A、B两信号,任意信号发送completed,新信号发送completed事件。

图例

Zip操作新信号发送事件的线程与触发线程一致。

CombineLatest:

Next事件:将来自A/B两信号的最新值组合成一个RACTuples,A、B任何一个信号发送值(如果另一个信号曾经发送过值)都会产生一个RACTuple并传递下去。
Error事件:A/B两信号,任意信号发送error事件,透传error事件。
Completed事件:A/B两信号,任意信号发送completed事件,透传completed事件。

图例

CombineLatest操作新信号发送事件的线程与触发线程一致。

斐波那契数列信号

在之前的一篇Post里提示出提到我们可以用函数式编程表示一个无限的数据结构如斐波那契数列、无限递增的数据结构。
那么在RAC中应该如何来做呢?

斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
RACSignal *repeat1 = [[RACSignal return:@1] repeat];



RACSignal *signalB = [repeat1 scanWithStart:RACTuplePack(@1, @1) reduce:^id(RACTuple *running, id _) {
NSNumber * next = @([running.first integerValue] + [running.second integerValue]);
return RACTuplePack(running.second, next);
}];

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

无限递增的数据结构

1
2
3
4
5
6
7
8
9
RACSignal *repeat1 = [[RACSignal return:@1] repeat];

RACSignal *signalB = [repeat1 scanWithStart:@0 reduce:^id(NSNumber * running, NSNumber * next) {
return @(running.integerValue + next.integerValue);
}];

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

To be continue…