ReactiveCocoa入门篇二
前言
通过之前入门篇一的学习,我们知道了怎么使用RAC以及基本的用法,这篇继续深入学习一下~
RAC的核心概念是信号,通过RACSignal
来表示,信号是数据流,可以被绑定和传递。
冷信号和热信号
冷热信号的概念源于.NET框架Reactive Extensions(RX)中的Hot Observable和Cold Observable,两者的区别是:
1. Hot Observable是主动的,尽管你并没有订阅事件,但是它会时刻推送,就像鼠标移动;而Cold Observable是被动的,只有当你订阅的时候,它才会发布消息。
2. Hot Observable可以有多个订阅者,是一对多,集合可以与订阅者共享信息;而Cold Observable只能一对一,当有不同的订阅者,消息是重新完整发送。
RAC框架各组件
- RACStream类
RACSignal的父类,本身意义不大,一般会以RACSignal或者RACSequence等这些更高层次的表现形态代替
- RACSignal类
RAC的核心就是signal,RACSignal有三种事件,通过-subscribeNext:error:completed:
可以对Next、Error、Completed三种事件作出相应反应,RACSignal可以发送任意多个Next事件,和一个Error或者Completed事件
- RACSubject类
可以认为是“可变的(Mutable)”信号/自定义信号,它嫁接非RAC代码到Signal世界的桥梁
-
RACCommand类
可以认为是回应某些动作的信号,通常触发该信号都是UI控件 -
RACSequence类
可以简单看做RAC世界的NSArray,RAC增加了-rac_sequence方法,可以使诸如NSArray这些集合类直接转换为RACSequence来使用。
- RACSeheduler类
RACScheduler类,类似GCD,但RACScheduler类支持撤销,并且总是运行安群的。RACScheduler是RAC里面对新城的简单封装,事件可以在指定的RACSchedler上分发和执行,不特殊指定的话,事件的分发和执行都在一个默认的后台线程里面做,大多数情况也就不用动了。有一些特殊的Signal必须在主线程调用,使用-deliverOn
可以切换调用的线程
创建信号
简单创建一个信号如下代码:
//创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"next"];
[subscriber sendCompleted];
return nil;
}];
//订阅信号
[signal subscribeNext:^(id x) {
NSLog(@"%@",x);
} error:^(NSError *error) {
NSLog(@"%@",error);
} completed:^{
NSLog(@"completed");
}];
输出如下:
2016-12-12 09:54:53.250 RACDemo[1183:33523] next
2016-12-12 09:54:53.250 RACDemo[1183:33523] completed
这样我们就创建了一个信号,通过subscriber
我们可以发送不同的next,error或者completed信心,然后在subscribeNext:
可以分别订阅到这些信息
常用用法
filter
filter可以起到过滤的作用,可以用来过滤掉一些无效信息
[[self.textField.rac_textSignal
filter:^BOOL(NSString*text){
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
输出如下:
2016-12-12 09:55:22.507 RACDemo[1183:33523] 1111
2016-12-12 09:55:23.142 RACDemo[1183:33523] 11111
2016-12-12 09:55:23.883 RACDemo[1183:33523] 111111
可以看到,只要textField里面的内容长度大于3的才会输出
map
map操作可以用来把接受的数据转换成想要的类型
[[[self.textField.rac_textSignal
map:^id(NSString*text){
return @(text.length);
}]
filter:^BOOL(NSNumber*length){
return[length integerValue] > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];
输出如下:
2016-12-12 10:12:01.591 RACDemo[1299:42629] 4
2016-12-12 10:12:03.551 RACDemo[1299:42629] 5
2016-12-12 10:12:04.399 RACDemo[1299:42629] 6
可以看到,map操作通过block改变了事件的数据。map从上一个next事件接收数据,通过执行block把返回值传给下一个next事件。
使用map操作来把接受的数据转换成想要的类型,只要是对象就行,如果map返回的是基本数据类型(如NSInteger),为了将它作为事件的内容,可以用@()封装成对象
既然说到了map,那么接下来顺便说说flattenMap
flattenMap
先看一下一段代码
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"next"];
return nil;
}];
[[[self.button
rac_signalForControlEvents:UIControlEventTouchUpInside]
map:^id(id x){
return signal;
}]
subscribeNext:^(id x){
NSLog(@"signal = %@", x);
}];
输出如下:
2016-12-12 10:22:34.721 RACDemo[1385:48349] signal = <RACDynamicSignal: 0x6080000337e0> name:
发现输出的是一个RACDynamicSignal
对象,不是我们想要的next
解决这个问题只要把上面的map改成flattenMap就可以了,输出如下:
2016-12-12 10:27:03.109 RACDemo[1414:50435] signal = next
这才是我想要的东西,为什么会这样呢?
这是因为map创建一个新的信号,信号的value是block(value),也就是说,如果block(value)是一个信号,那么就是信号的value仍然是信号。如果是flattenMap则会继续调用这个信号的value,作为新的信号的value
RAC宏
RAC宏允许直接把信号的输出应用到对象的属性上。RAC有两个参数,第一个是设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。
RACSignal *textSignal = [self.textField.rac_textSignal
map:^id _Nullable(NSString * _Nullable value) {
return value;
}];
RAC(self.textField, backgroundColor) =
[textSignal
map:^id(NSString *str){
return [str length]>3 ? [UIColor blueColor]:[UIColor yellowColor];
}];
可以看到,当textField里输入内容长度大于3的时候,textField的背景就会变成蓝色,否则就是黄色
combineLatest
//定义2个自定义信号
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
//组合信号
[[RACSignal combineLatest:@[letters, numbers]
reduce:^(NSString *letter, NSString *number){
//把2个信号的信号值进行字符串拼接
return [letter stringByAppendingString:number];
}] subscribeNext:^(NSString * x) {
NSLog(@"%@", x);
}];
//自己控制发送信号值
[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];//打印B1
[letters sendNext:@"C"];//打印C1
[numbers sendNext:@"2"];//打印C2
RACSignal的combineLatest:reduce:方法可以聚合任意数量的信号,reduce block的参数和每个源信号有关,当其中任何一个源信号产生新值时,reduce block都会执行,block的返回值会发给下一个信号
- 聚合——多个信号可以聚合成一个新的信号,实际上可以聚合并产生任何类型的信号
throttle
throttle可以起到节流作用
在我们做搜索框的时候,有时候需求的时实时搜索,即用户每每输入字符,view都要求展现搜索结果。这时如果用户搜索的字符串较长,那么由于网络请求的延时可能造成UI显示错误,并且多次不必要的请求还会加大服务器的压力,这显然是不合理的,此时我们就需要用到节流。
[[[self.textField rac_textSignal] throttle:0.5 ] subscribeNext:^(NSString * _Nullable x) {
NSLog(@"%@",x);
}];
}];
takeUntil
takeUntil方法:在给定signal完成前一直取值
//创建取值信号
RACSignal *takeSignal = [RACSignal createSignal:^RACDisposable *(id subscriber) {
//创建一个定时器信号,每隔1秒触发一次
RACSignal *signal = [RACSignal interval:1
onScheduler:[RACScheduler mainThreadScheduler]];
//定时接收
[signal subscribeNext:^(id x) {
[subscriber sendNext:@"等等~"];
}];
return nil;
}];
//创建条件信号
RACSignal *conditionSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"时间到了");
[subscriber sendCompleted];
});
return nil;
}];
//设置条件,塔克Signal信号是在conditionSignal信号接收完成前,不断地取值
[[takeSignal takeUntil:conditionSignal] subscribeNext:^(id _Nullable x) {
NSLog(@"%@",x);
}];
输出如下:
2016-12-12 11:49:50.209 RACDemo[1938:91165] 等等~
2016-12-12 11:49:51.209 RACDemo[1938:91165] 等等~
2016-12-12 11:49:52.209 RACDemo[1938:91165] 等等~
2016-12-12 11:49:53.209 RACDemo[1938:91165] 等等~
2016-12-12 11:49:54.208 RACDemo[1938:91165] 等等~
2016-12-12 11:49:54.208 RACDemo[1938:91165] 时间到了
ignore
忽略信号,执行一个任意类型的数据,在发送时进行判断,如果相同该信号就会被忽略发送
[[[self.textField rac_textSignal] ignore:@"111"] subscribeNext:^(NSString * _Nullable x) {
NSLog(@"%@",x);
}];
输出如下:
2016-12-12 12:08:57.251 RACDemo[2028:99269] 1
2016-12-12 12:08:57.812 RACDemo[2028:99269] 11
2016-12-12 12:08:58.779 RACDemo[2028:99269] 1111
这样111就会被忽略了
zipWith
zipWith能起到合并压缩的作用
//创建信号A
RACSignal *signalA = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"天真活泼"];
return nil;
}];
//创建信号B
RACSignal *signalB = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
[subscriber sendNext:@"机智可爱"];
return nil;
}];
//合并后出来的是压缩包,需要解压才能取得到里面的值
[[signalA zipWith:signalB] subscribeNext:^(id _Nullable x) {
//解压缩
RACTupleUnpack(NSString *stringA,NSString *stringB) = x;
NSLog(@"我是一个%@%@的孩子",stringA,stringB);
}];
输出:
2016-12-12 12:18:14.808 RACDemo[2089:103473] 我是一个天真活泼机智可爱的孩子
then
then可以起到维持秩序的作用,then方法会等待completed事件的发送,然后在订阅有then block返回的signal。这样就高效地把控制权从一个signal传递给下一个
[[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"第一步");
[subscriber sendCompleted];
return nil;
}] then:^RACSignal *{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"第二步");
[subscriber sendCompleted];
return nil;
}];
}] then:^RACSignal *{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"第三步");
[subscriber sendCompleted];
return nil;
}];
}] subscribeCompleted:^{
NSLog(@"完成");
}];
输出如下:
2016-12-12 12:23:15.482 RACDemo[2148:107451] 第一步
2016-12-12 12:23:15.482 RACDemo[2148:107451] 第二步
2016-12-12 12:23:15.483 RACDemo[2148:107451] 第三步
2016-12-12 12:23:15.483 RACDemo[2148:107451] 完成
More
内存管理
在之前写的代码中,RAC的信号处理过程就像一个管道一样,ReactiveCocoa设计的一个目标就是支持匿名生成管道这种编程风格。那个这些管道是如何持有的呢?显然,它并没有分配某个变量或是属性,所以他也不会有引用计数的增加,那么它是怎么销毁的呢?
为了支持这种模型,RAC自己持有全局的所有信号,如果一个signal有一个或多个订阅者,那这个signal就是活跃的。如果所有的订阅者都被移除了,那这个信号就能被销毁了。
那么问题来了:如何取消订阅一个signal?在一个completed或者error事件之后,订阅会自动移除。你还可以通过RACDisposable 手动移除订阅。
@weakify和@strongify
RAC定义了@weakify和@strongify这两个宏,@weakify宏让你创建一个弱应用的影子对象(如果需要多个弱引用,可以传入多个变量),@strongify让你创建一个对之前传入@weakify对象的强引用。利用它们可以解决循环引用问题,如下代码:
@weakify(self)
[[self.textField.rac_textSignal
map:^id(NSString *text) {
return [text length] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
@strongify(self)
self.textField.backgroundColor = color;
}];
上面代码中subscribeNext:block中使用了self来获取textField的引用。block会捕获并持有其作用域内的值。因此,如果self和这个信号之间存在一个强引用的话,就有可能造成循环引用
总结
RAC要学习的内容还有很多,越往深学越觉得自己学得很肤浅,我这里只是稍微深入学习了一些基本概念和基本用法,还有其他很多高深用法还没有用到,等以后继续学习了在分享出来