iOS ReactiveCocoa(RAC)番外篇
在上一篇ReactiveCocoa(RAC)教程中,我们学习了ReactiveCocoa的一些基础内容。本篇中我们继续介绍一些有趣的用法。
NSObject相关的信号
在基础教程中我们知道,ReactiveCocoa使用分类扩展一套标准UI库的信号,如文本框的rac_textSignal
、按钮的rac_signalForControlEvents:
等,也有通过多个信号生成组合信号。除此之外,ReactiveCocoa同样对NSObject类进行了多种扩展,其中包括类似KVO形式的实现,属性监听信号,方法调用信号。当然,这些信号是基于createSignal:
来创建的。
KVO扩展
在 NSObject+RACKVOWrapper.h
文件中,ReactiveCocoa 实现了对实例属性KVO的实现。话不多说,我们先来体验一下。
首先需要在文件头部导入该分类:
#import <ReactiveCocoa/NSObject+RACKVOWrapper.h>
原因是ReactiveCocoa其实优化了属性监听,有更好的方式去监听对象属性,但是实现是基于该文件的,所以这里还是介绍一下。
创建一个名为Person
的类:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (strong, nonatomic) NSString* name;
@end
@implementation Person
@end
在控制器中导入类Person.h
,然后创建一个实例并初始化:
@property (strong, nonatomic)Person* p;
- (void)viewDidLoad {
[super viewDidLoad];
self.p = [Person new];
}
现在我们来监听实例p
的name
属性变化,在文件viewDidLoad
方法中继续添加一下代码:
[self.p rac_observeKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld observer:nil block:^(id value, NSDictionary *change) {
NSLog(@"value:%@",value);
NSLog(@"NSDictionary:%@",change);
}];
// 修改值,触发监听回调
self.p.name = @"Joy";
运行应用程序,控制台会输出:
value:Joy
NSDictionary:{
RACKeyValueChangeAffectedOnlyLastComponentKey = 1;
RACKeyValueChangeCausedByDeallocationKey = 0;
kind = 1;
new = Joy;
old = "<null>";
}
酷~这么简单?就是这么简单!
在经典的KVO设计模式中,我们需要为被观察者添加观察对象addObserver:forKeyPath:options:context:
,并且需要实现其代理方法observeValueForKeyPath:ofObject:change:context:
,还需要注意循环引用和释放问题,如果需要监听的对象过多,还需要分门别类的进行区分等等,代理的形式的缺陷就是不够直观,而ReactiveCocoa通过回调的方式让整个业务更直观。
观察没有必要明确移除,在观察者或者接收者释放时将被删除
属性监听
为了将KVO统一,让其符合ReactiveCocoa的信号流特点,通常我们并不会直接采用1.1中的形式,这也就是为什么ReactiveCocoa将其放在头文件中。ReactiveCocoa对NSObject的还有其他方法,可以用来创建RACSignal
用于管道的构建。
[[self.p rac_valuesAndChangesForKeyPath:@"name" options:NSKeyValueObservingOptionNew observer:nil]
subscribeNext:^(id x) {
NSLog(@"x:%@",x);
}];
self.p.name = @"Joy";
运行项目,控制台输出:
x:<RACTuple: 0x600003828550> (
Joy,
{
RACKeyValueChangeAffectedOnlyLastComponentKey = 1;
RACKeyValueChangeCausedByDeallocationKey = 0;
kind = 1;
new = Joy;
}
)
发现输出的内容是RACTuple
类型的内容,它实际上是ReactiveCocoa自己实现的元组类型(OC中没有元组),类似Swift、python等语言中元组。
我们修改一下代码,使用RACTuple
来接收数据:
[[self.p rac_valuesAndChangesForKeyPath:@"name" options:NSKeyValueObservingOptionNew observer:nil]
subscribeNext:^(RACTuple* x) {
NSLog(@"value:%@ \n dic:%@",x.first, x.second);
}];
self.p.name = @"Joy";
运行项目,控制台输出:
value:Joy
dic:{
RACKeyValueChangeAffectedOnlyLastComponentKey = 1;
RACKeyValueChangeCausedByDeallocationKey = 0;
kind = 1;
new = Joy;
}
我们看到,输出结果和1.1中的结果一致,但是上述的例子中是一个最简单的管道结构,通过信号将属性的变化流向其订阅者。也你可以将该信号添加到已存在的其他管道中,实现更复杂的数据流。
优化RACObserve
在属性监听时,我们通常只关心新值,其他状态的值并不关心,没关系,ReactiveCocoa也同样考虑到了这一点,它还有一个更简化的方法:
[[self.p rac_valuesForKeyPath:@"name" observer:nil]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
self.p.name = @"Joy";
运行项目,控制台输出:
(null)
Joy
可以看到,使用了这个简化方法后,数据流传过来的最终值x
只有新值,但是敏锐的你可能要问:为什么只修改一次,订阅块却被执行了两次?
我们点开该方法的实现部分:
- (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath observer:(NSObject *)observer {
return [[[self rac_valuesAndChangesForKeyPath:keyPath options:NSKeyValueObservingOptionInitial observer:observer] reduceEach:^(id value, NSDictionary *change) {
return value;
}] setNameWithFormat:@"RACObserve(%@, %@)", self.rac_description, keyPath];
}
我们看到该方法在调用rac_valuesAndChangesForKeyPath:options:observer:
时,options
给的是NSKeyValueObservingOptionInitial
,该选项使得初始化的时候的改变也同样会触发回调。
不过没有关系,ReactiveCocoa提供了一个方法,用来跳过指定次数的回调skip:
,我们来修改一下代码:
[[[self.p rac_valuesForKeyPath:@"name" observer:nil] skip:1]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
self.p.name = @"Joy";
skip:n
表示跳过第n次的next事件,其他照旧。
运行项目,控制台输出:
Joy
很好,我们得到了最想要的东西。似乎没什么可以说的了,不!ReactiveCocoa 并不满足于此,ReactiveCocoa中有非常多的宏,用来快速使用一些功能,没错,上述代码通样有自己的宏:
[[RACObserve(self.p, name) skip:1]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
self.p.name = @"Joy";
运行项目,控制台会输出同样的结果。上述代码中的RACObserve
是你在项目经常使用到的。
注意到RACObserve
宏,
#define RACObserve(TARGET, KEYPATH) \
[(id)(TARGET) rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]
它接收一个对象,和其keypath
,因此上述代码同样可以修改为:
[[RACObserve(self, p.name) skip:1]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
self.p.name = @"Joy";
方法监听
以为做到KVO转换就满足了吗?不!ReactiveCocoa为了将数据流的概念深入推进,还做到了将方法转换为信号,将方法调用进行了监听。
我们给Person
添加一个方法,用于后面演示。
@interface Person : NSObject
@property (strong, nonatomic) NSString* name;
-(void)say:(NSString*)some;
@end
@implementation Person
-(void)say:(NSString *)some{
NSLog(@"%@",some);
}
@end
在viewDidLoad
中继续添加:
[[self.p rac_signalForSelector:@selector(say:)]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
// 某个时刻
[self.p say:@"hello world!"];
运行项目,控制台输出:
hello world!
<RACTuple: 0x600000ed0500> (
"hello world!"
)
项目先输出了方法say:
方法的结果hello world!
,在我们订阅块中,得到了一个元组类型,其中包括了方法say:
方法的参数。
操作事件监听
针对UIControl
类,ReactiveCocoa也进行了扩展,让我们能够快速的得到各种事件的回调。以我们常用的按钮控件来演示:
// 按钮的点击事件
[[self.myButton rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
// do something ...
}];
非RAC和RAC的桥梁 : RACSubject
RACSubject
是RACSignal
的子类,并且遵循了协议RACSubscriber
,这让你可以手动控制发送next、completed和error事件,让非RAC事件转为RAC事件。使用RACSubject
可以让您替代回调块、代理等,使用起来更像是回调块,可作为参数、属性、返回值来使用,但是和回调块的一对一不同,RACSubject
可以添加个订阅块,当发送next事件时,每个订阅块都可以收到数据流。
// 创建一个RACSubject对象
RACSubject* subject = [RACSubject subject];
// 添加订阅1
[subject subscribeNext:^(id x) {
NSLog(@"1:%@",x);
}];
// 添加订阅2
[subject subscribeNext:^(id x) {
NSLog(@"2:%@",x);
}];
// 完成事件
[subject subscribeCompleted:^{
NSLog(@"完成");
}];
// 某个时刻
[subject sendNext:@[@"data1",@"data2"]];
[subject sendCompleted];
运行项目,控制台输出:
1:(
data1,
data2
)
2:(
data1,
data2
)
完成
我们看到在subject
发送了一个next事件,两个订阅者都收到了相同的数据。
在只有提前订阅的块才能收到RACSubject发出的next事件。另外
RACSubject
是单向的,它无法收到订阅块的消息,如果你需要发送具有反馈的消息,你可能需要使用到RACCommand
。
双向事件 RACCommand
RACCommand
和RACSubject
不同,RACSubject
是单向传递事件,而当你发送一则消息并且需要反馈时,RACSubject
通常不行,但是你可以使用RACCommand
来实现双向消息。
// command的创建需要一个信号,这个信号用来反向传递数据
RACCommand* command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
NSLog(@"%@",input);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 模拟网络请求
[RACScheduler.currentScheduler afterDelay:1.0 schedule:^{
[RACScheduler.mainThreadScheduler schedule:^{
[subscriber sendNext:@"RACSignal信号发出消息。"];
[subscriber sendCompleted];
}];
}];
return nil;
}];
}];
// 添加订阅,由于command传递过来的是创建时的那个信号,所以我们使用信号进行订阅
[command.executionSignals subscribeNext:^(RACSignal* x) {
// 订阅
[x subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
}];
// // 转换为最新的信号
// [command.executionSignals.switchToLatest subscribeNext:^(id x) {
// NSLog(@"%@",x);
// }];
// command传递信息
[command execute:@"RACCommand传递过来的信息"];
运行项目,控制台输出:
RACCommand传递过来的信息
RACSignal信号发出消息。
RACCommand
的创建需要你提供是一个RACSignal
信号,这个信号用来反馈数据,如网络请求、本地数据库读写等等,另外,block
中的参数input
是RACCommand
执行时传入的数据,即你正向传递的数据(相对内部的信号)。
在创建完成之后,我们使用RACCommand
的属性executionSignals
进行订阅,因为你提供的是信号,所以数据流过来的还是信号,因此需要你使用信号接收并再次进行订阅,当然你也可以使用switchToLatest
转换信号中的信号,直接获取最终数据。
最后,在某个合适的时机使用execute:
方法执行RACCommand
。
如果你想要在RACCommand
执行时做一些等待提示操作,并在执行后取消提示,你则可以这样:
[command.executionSignals subscribeNext:^(RACSignal* x) {
// 开启提示
[x subscribeNext:^(id x) {
// 结束提示
// Do something...
}];
}];
errors 错误事件
如果RACCommad
内部信号发出error事件,你无法通过以下代码获取到错误。
首先修改内部信号,发出error事件:
// command的创建需要一个信号,这个信号用来反向传递数据
RACCommand* command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
NSLog(@"%@",input);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 模拟网络请求
[RACScheduler.currentScheduler afterDelay:1.0 schedule:^{
[RACScheduler.mainThreadScheduler schedule:^{
// [subscriber sendNext:@"RACSignal信号发出消息。"];
// [subscriber sendCompleted];
[subscriber sendError:[[NSError alloc] initWithDomain:@"error" code:-100 userInfo:@{@"value":@"发生错误。"}]];
}];
}];
return nil;
}];
}];
尝试订阅错误:
[command.executionSignals.switchToLatest subscribeNext:^(id x) {
NSLog(@"%@",x);
} error:^(NSError *error) {
NSLog(@"%@",error);
}];
运行应用程序,你会发现没有收到任何消息。
正确的方式是使用RACCommand
中的另一个属性errors
,它也是一个信号类型,用来传递发生错误信息。
// RACCommand的错误事件
[command.errors subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
运行应用程序,控制台输出:
Error Domain=error Code=-100 "(null)" UserInfo={value=发生错误。}
合起来
除了使用属性executionSignals
拿到内部信号并进行订阅,我们注意执行方法execute:
有返回值并且是一个RACSignal
类型的对象。实际上,这个返回对象就是那个内部信号,我们可以使用它直接进行订阅:
// command的创建需要一个信号,这个信号用来反向传递数据
RACCommand* command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
NSLog(@"%@",input);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 模拟网络请求
[RACScheduler.currentScheduler afterDelay:1.0 schedule:^{
[RACScheduler.mainThreadScheduler schedule:^{
[subscriber sendNext:@"RACSignal信号发出消息。"];
[subscriber sendError:[[NSError alloc] initWithDomain:@"error" code:-100 userInfo:@{@"value":@"发生错误。"}]];
}];
}];
return nil;
}];
}];
// command传递信息
[[command execute:@"RACCommand传递过来的信息"] subscribeNext:^(id x) {
NSLog(@"%@",x);
} error:^(NSError *error) {
NSLog(@"%@",error);
}];
运行应用程序,控制台输出:
RACCommand传递过来的信息
RACSignal信号发出消息。
Error Domain=error Code=-100 "(null)" UserInfo={value=发生错误。}
另一个初始化方法
RACCommand
初始化一共有两种,另外一种:
- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock;
该初始化方法除了需要你传入一个反馈信号之外,还需要传入一个传递布尔值事件的RACSignal
,这个信号作用是过滤,当传递的布尔值为真时,command能够执行,反之则不行。
UIButton的扩展属性rac_command
如果绑定的是使用上述方式创建的command,那么button的enable
属性会随着command的可执行性而改变,意思是当传递布尔值为真时,按钮才能使用。另外,当你按下按钮,command开始执行时,按钮的enable被自动设置成来NO
,除非command执行完成,即发生completed事件。
当UIbutton的
rac_command
已经绑定了上述方法生成的command,那么你就不能动态改变按钮的enable
。RAC(self.button, enable) = someSignal;
UIButton 的 rac_command
在UIButton+RACCommandSupport.h
中,给UIButton进行了扩展rac_command
,给按钮绑定上RACCommand
,可以在按钮点击时自动执行execute:
。
self.signInButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@"按钮执行结果"];
[subscriber sendCompleted];
return nil;
}];
}];
[self.signInButton.rac_command.executionSignals.switchToLatest subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
运行应用程序,在点击按钮时,控制台会输出:
按钮执行结果
如果你使用的是带有enabledSignal
参数创建的RACCommand
,它还会自动使你的按钮enable
参数变化,直到你在信号中的事件完成并发生completed和error事件。
// 创建 enabledSignal, 按钮可用 signal
RACSignal* signal = [self.usernameTextField.rac_textSignal map:^id(id value) {
return @([value length]>3);
}];
self.signInButton.rac_command = [[RACCommand alloc] initWithEnabled:signal signalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:@"按钮执行结果"];
[subscriber sendCompleted];
// [subscriber sendError:[NSError errorWithDomain:@"https://" code:-1 userInfo:@{}]];
});
return nil;
}];
}];
[self.signInButton.rac_command.executionSignals.switchToLatest subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
只有在文本框中输入的字符大于3个时,按钮才能够使用。
RACCommand和RACSubject的差异
-
RACSubject
只能单向发送事件,发送者(被监听者)将事件发送出去,让接收者们接收事件后进行处理。 -
RACCommand
是双向的,常用作网络请求、操作数据库读写、按钮的操作事件等。当你想要向某个对象发送消息(操作),并需要该对象作出反馈时,你需要用到RACCommand
,其中的内部信号将反馈变成了可能。
下面使用一张图来表明两者的差别和使用情况:
RACSubject与RACCommandRACScheduler
// 默认优先级下的调度运行
[RACScheduler.scheduler schedule:^{
// 获取当前调度的名称
NSLog(@"%@",[[RACScheduler currentScheduler] valueForKey:@"name"]);
// 回到主线程更新UI等
[RACScheduler.mainThreadScheduler schedule:^{
// do something ...
}];
}];
RACScheduler
是一个线性执行队列,ReactiveCocoa 中的信号可以在RACScheduler
上执行任务、发送结果。
RACScheduler
类的内部只有一个用于追踪标记和debug的属性name
,你可以通过KVC的方式访问该属性。
我们可以将RACScheduler
中的方法分为两类,一类是用于初始化RACScheduler
实例的初始化方法:
+immediateScheduler //立刻执行
+mainThreadScheduler //主线程
+schedulerWithPriority:name: //自定义
+currentScheduler //当前的
其中+schedulerWithPriority:name:
衍生出了几个具有默认参数的快捷方式,你可以在RACScheduler.h
找到。
另外一类就是用于调度、执行任务的方法:
-schedule:
-after:schedule:
-afterDelay:schedule:
-after:repeatingEvery:withLeeway:schedule:
-scheduleRecursiveBlock:
其他一些示例:
// 低优先级下的调度运行
[[RACScheduler schedulerWithPriority:RACSchedulerPriorityLow name:@"custom scheduler"]
schedule:^{
NSLog(@"%@",[[RACScheduler currentScheduler] valueForKey:@"name"]);
}];
// 延迟指定时长进行操作
[RACScheduler.scheduler afterDelay:5.0 schedule:^{
NSLog(@"延迟5秒后输出");
}];
// 指定时间操作
[RACScheduler.scheduler after:[NSDate dateWithTimeIntervalSinceNow:2.0] schedule:^{
NSLog(@"指定2秒后输出");
}];
// 定时器
[RACScheduler.currentScheduler after:[NSDate date] repeatingEvery:1.0 withLeeway:0 schedule:^{
NSLog(@"重复输出");
}];
方法
-after:repeatingEvery:withLeeway:schedule:
中明确指出,在+ immediateScheduler
上调用此方法被视为未定义的行为。
RACSignal中的定时器
在上一节中介绍了RACScheduler
,其中演示了定时器的使用。ReactiveCocoa
也同样给信号扩展了类似的方法,从信号的角度来执行定时调度的任务。
+interval:onScheduler:
+interval:onScheduler:withLeeway:
使用示例:
[[RACSignal interval:2.0 onScheduler:RACScheduler.currentScheduler withLeeway:10]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
运行应用程序,控制台输出:
Mon Sep 16 11:19:58 2019
Mon Sep 16 11:20:00 2019
Mon Sep 16 11:20:03 2019
Mon Sep 16 11:20:05 2019
Mon Sep 16 11:20:06 2019
Mon Sep 16 11:20:08 2019
Mon Sep 16 11:20:11 2019
Mon Sep 16 11:20:13 2019
从上面看出,RACSignal
传递过来的是每次调用的日期数据,另外,我们发现,在Leeway
延迟执行时间为10秒时,重复执行的时间间隔非常的不稳定,你可以注意你的控制台输出的前半部分的详情输出时间,你会发现误差超过了0.5秒以上。该方法注释中也指出了这个问题Note that some additional latency is to be expected, even when specifying a
leewayof 0.
。即使指定leeway
为0,也可能存在预期一些额外的延迟。
这可能是RACScheduler
内部的消耗使时间误差加大,但是如果你使用场景不需要如此精确度,还是可以方便地作为定时器来使用。
onScheduler:
参数不能为nil
以及RACScheduler.immediateScheduler
。
取消定时器任务:
RACDisposable* disposable = [[RACSignal interval:2.0 onScheduler:RACScheduler.currentScheduler withLeeway:10]
subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
// 将来的某个时刻
[disposable dispose];
总结
ReactiveCocoa 对 KVO 的实现,让我们方便的监听对象的属性变化,因为这一点,ReactiveCocoa 成为 MVVM 结构的基础让我们所熟知。
RACSubject 和 RACCommand 是非 RAC 转为 RAC 的桥梁,前者可以手动控制发送事件,它的应用类似通知,可以被多次订阅,消息则发给多个订阅者。后者属于双向事件,消息并且需要反馈时使用它,它的应用场景如异步请求事件、操作数据库等。
RACScheduler 是 ReactiveCocoa 的关于线程调度对象,和事件流所被驱动的线程有关,是一个副产品。