前言
最近第一次接触了一个采用MVVM架构的项目。有很多人学习RAC应该是为了适应MVVM。关于MVVM推荐一篇文章->iOS 关于MVVM With ReactiveCocoa设计模式的那些事。说得挺好,但完全不会函数响应式编程的我,看到后面部分就止步了。 MVVM这种概念性的东西,如果不实操,看完都学不到什么。于是先学习RAC,发现网上教程大多都看不懂???在某文章翻到这样一句hhhhhhhh。
为了加深理解,故从初学者的角度写下这篇笔记,显得啰嗦。希望能带给小白们something helpful。
本文先不管RAC有什么用,入门学习,下一篇文章笔者再尽能力展示RAC的应用。
ReactiveCocoa入门学习
笔者整理了一份笔记。开发时可以对着想用什么,熟了以后就不用看了。
RACSignal
RAC中常用的类是RACSignal,据说会用就能开发了。所以这里探讨其实现。
大多教程都是说三步。
// RACSignal使用步骤:
// 1.创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id subscriber))didSubscribe
// 2.订阅信号,才会激活信号. - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
// 3.发送信号 - (void)sendNext:(id)value
复制代码
然后给出代码
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// block调用时刻:每当有订阅者订阅信号,就会调用block。
NSLog(@"信号被订阅");
// 2.发送信号
[subscriber sendNext:@1];
// 如果不在发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
// block调用时刻:当信号发送完成或者发送错误,就会自动执行这个block,取消订阅信号。
// 执行完Block后,当前信号就不在被订阅了。
NSLog(@"信号被取消订阅");
}];
}];
// 3.订阅信号,才会激活信号.
[signal subscribeNext:^(id x) {
// block调用时刻:每当有信号发出数据,就会调用block.
NSLog(@"接收到数据:%@",x);
}];
复制代码
看上去觉得很好用,但仔细想想这执行顺序是怎么回事?
笔者整理了一张图,可以本地打开这图,对照着下文理解。
只要能理解这图,RAC的实现就不会显得迷茫。
- 创建信号
我们先从+ (RACSignal *)createSignal:(RACDisposable * (^)(id subscriber))didSubscribe
探讨
注意到block的参数中RACSubscriber
是个协议,后面也有一个相同的类名。
该方法将blockdidSubscribe
传进了另一个类RACDynamicSignal
,其是RACSignal的子类,最直接的区别是其有一个block成员变量。
// RACDynamicSignal.h
@interface RACDynamicSignal : RACSignal
+ (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe;
@end
// RACDynamicSignal.m
@interface RACDynamicSignal ()
// The block to invoke for each subscriber.
@property (nonatomic, copy, readonly) RACDisposable * (^didSubscribe)(id<RACSubscriber> subscriber);
@end
复制代码
原来生成了一个RACDynamicSignal
的对象,并把didiSubscribe赋给了成员变量,最终返回该对象。这个block将在第二步中被调用。
所以,运行时signal实际上属于RACDynamicSignal
。
- 订阅信号
在生活中,信号只有被订阅。订阅说起来就像某个订阅者调用了订阅信号方法一样 [subscriber subscribe:signal]
;但订阅方法事实上是信号对象本身找到订阅者[singnal subscribe:subscriber]
。。。
所以笔者认为这里理解为激活信号更妥当。
上面提到signal实际上属于RACDynamicSignal
,所以查看方法时不要跳转错了。
可以看到生成了一个RACSubscriber
的对象o
,并把代码块赋给了成员变量。然后o
作为参数被调用[signal subscribe:o]
。
⚠️⚠️⚠️在[signal subscribe]
中。发现o
居然从RACSubscriber
类变成了RACPassthroughSubscriber
类,其有个成员变量_innerSubscriber
保存着原本的RACSubscriber
。
然后,第一步中signal里保存的成员变量didSubscribe
,在这会被执行 self.didSubscribe(subscriber)
。
- 发送信号
执行didSubscribe
时,若块有这么一句[subscriber sendNext:@1]
就会发送信号。
转到````self.innerSubscriber sendNext:value],其之前保存的块
next``在这被调用。
最后再总结一下
- 只要订阅RACDynamicSignal,就会执行didSubscribe
- 只要订阅者调用sendNext,就会执行nextBlock
到这我们已经基本理清这错综复杂的顺序。
RACDisposable
上面didSubscribe
返回类型为RACDisposable
,现在开始研究这个类。
- 作用:用于取消订阅或者清理资源。当信号发送完成或者发送错误的时候,就会触发它。
还是看回以上的代码
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// block调用时刻:每当有订阅者订阅信号,就会调用block。
NSLog(@"信号被订阅");
// 2.发送信号
[subscriber sendNext:@1];
// 如果不在发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
// block调用时刻:当信号发送完成或者发送错误,就会自动执行这个block,取消订阅信号。
// 执行完Block后,当前信号就不在被订阅了。
NSLog(@"信号被取消订阅");
}];
}];
// 3.订阅信号,才会激活信号.
[signal subscribeNext:^(id x) {
// block调用时刻:每当有信号发出数据,就会调用block.
NSLog(@"接收到数据:%@",x);
}];
复制代码
运行结果告诉我们,有输出"信号被取消订阅"。原因是默认一个信号发送完毕后,订阅者会主动取消订阅,即订阅者引用数-1。
那要怎么才能不自动取消呢?只要在控制器中声明一个属性,引用数+1。
@property (nonatomic, strong) id<RACSubscriber> subscriber;
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// block调用时刻:每当有订阅者订阅信号,就会调用block。
_subscriber = subscriber;
NSLog(@"信号被订阅");
// 2.发送信号
[subscriber sendNext:@1];
// 如果不在发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
// block调用时刻:当信号发送完成或者发送错误,就会自动执行这个block,取消订阅信号。
// 执行完Block后,当前信号就不在被订阅了。
NSLog(@"信号被取消订阅");
}];
}];
复制代码
这时就要手动取消订阅,然后会调用“信号被取消订阅”的block。
RACDisposable *disposable = [signal subscribeNext:^(id x) {
// block调用时刻:每当有信号发出数据,就会调用block.
NSLog(@"接收到数据:%@",x);
}];
[disposable dispose];
复制代码
看完不知道有什么用也没关系,笔者将在下一篇应用中再次提起。
RACSubject
- 作用:自己可以充当信号,又能发送信号。
// 1.创建信号
RACSubject *subject = [RACSubject subject];
// 2.订阅信号
[subject subscribeNext:^(id x) {
// block调用时刻:当信号发出新值,就会调用.
NSLog(@"第一个订阅者%@",x);
}];
[subject subscribeNext:^(id x) {
// block调用时刻:当信号发出新值,就会调用.
NSLog(@"第二个订阅者%@",x);
}];
// 3.发送信号
[subject sendNext:@"1"];
复制代码
-
和RACSignal相同点
- 都是三步。
- 订阅者都是把
next
存起来。
-
和RACSignal的区别
RACSubject
没有didSubscribe
。RACSubject
对象中有一个数组,用来存订阅者。订阅者被创建后将被添加进该数组。RACSubject
发送信号后,会遍历数组,执行next
。
-
其还有个子类
RACReplaySubject
,区别是RACReplaySubject
可以先发送后订阅,而RACSubject
不能。
这么看来,感觉RACSubject
更容易理解hhh。而且其能做到RACSignal
不能做到的,想不懂为什么各资源都是先学RACSignal
。有人能解释下为什么最常用类反而不是最轻松的呢?是性能问题吗?
- 应用场景:通常用来代替代理。
笔者偷懒,就复制别人的代码了。感觉看懂上面后,下面这段代码没什么难度。
// 需求:
// 1.给当前控制器添加一个按钮,modal到另一个控制器界面
// 2.另一个控制器view中有个按钮,点击按钮,通知当前控制器
步骤一:在第二个控制器.h,添加一个RACSubject代替代理。
@interface TwoViewController : UIViewController
@property (nonatomic, strong) RACSubject *delegateSignal;
@end
步骤二:监听第二个控制器按钮点击
@implementation TwoViewController
- (IBAction)notice:(id)sender {
// 通知第一个控制器,告诉它,按钮被点了
// 通知代理
// 判断代理信号是否有值
if (self.delegateSignal) {
// 有值,才需要通知
[self.delegateSignal sendNext:nil];
}
}
@end
步骤三:在第一个控制器中,监听跳转按钮,给第二个控制器的代理信号赋值,并且监听.
@implementation OneViewController
- (IBAction)btnClick:(id)sender {
// 创建第二个控制器
TwoViewController *twoVc = [[TwoViewController alloc] init];
// 设置代理信号
twoVc.delegateSignal = [RACSubject subject];
// 订阅代理信号
[twoVc.delegateSignal subscribeNext:^(id x) {
NSLog(@"点击了通知按钮");
}];
// 跳转到第二个控制器
[self presentViewController:twoVc animated:YES completion:nil];
}
@end
作者:袁峥
链接:https://www.jianshu.com/p/87ef6720a096
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
复制代码
留个任务给大家:为什么这里不用RACSignal
?
提示:RACSubject
自己可以充当信号,又能发送信号。
RACTuple
- 作用:元组类,相当于NSArray。
// 元组
RACTuple *tuple = [RACTuple tupleWithObjectsFromArray:@[@"1",@"2",@1]];
NSString *str1 = tuple[0];
NSString *str2 = tuple[1];
NSNumber *number = tuple[2];
复制代码
RACSequence
- 作用:用于代替NSArray,NSDictionary,可以使用它来快速遍历数组和字典。开发中遇到NSArray和NSDictionary都用其代替。
// 遍历数组
NSArray *arr = @[@"213",@"321",@1];
[arr.rac_sequence.signal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
链式函数分开来如下
// RAC集合
// RACSequence *sequence = arr.rac_sequence;
//
// // 把集合转换成信号
// RACSignal *signal = sequence.signal;
//
// // 订阅集合信号,内部会自动遍历所有的元素发出来
// [signal subscribeNext:^(id x) {
// NSLog(@"%@",x);
// }];
复制代码
遍历字典有一个强大的宏RACTupleUnpack
。
注意这里x
的类型为RACTuple
。笔者在这里直接把id x
改成RACTuple *x
。
// 遍历字典
NSDictionary *dict = @{@"account":@"123",@"name":@"hsu",@"age":@18};
// RAC集合
[dict.rac_sequence.signal subscribeNext:^(RACTuple *x) {
// NSString *key = x[0];
// NSString *value = x[1];
// NSLog(@"%@ %@",key,value);
// RACTupleUnpack:用来解析元组
// 宏里面的参数,传需要解析出来的变量名
// = 右边,放需要解析的元组
RACTupleUnpack(NSString *key,NSString *value) = x;
NSLog(@"%@ %@",key,value);
}];
复制代码
// 字典数组转模型
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];
NSArray *dictArr = [NSArray arrayWithContentsOfFile:filePath];
// 方法一
// NSMutableArray *arr = [NSMutableArray array];
// [dictArr.rac_sequence.signal subscribeNext:^(NSDictionary *x) {
// Flag *flag = [Flag flagWithDict:x];
// [arr addObject:flag];
// }];
// 方法二
// 高级用法
// 会把集合中所有元素都映射成一个新的对象
NSArray *arr = [[dictArr.rac_sequence map:^id(NSDictionary *value) {
// value:集合中元素
// id:返回对象就是映射的值
return [Flag flagWithDict:value];
}] array];
复制代码
RACMulticastConnection
- 作用:不管信号被订阅多少次,只调用一次block
didSubscribe
。
试想一下这种场景,如果didSubscribe
中,请求了一个网络数据。每次订阅不想再去网络获取。
则要达到的效果是:只请求一次,但订阅者的blocknext
都要调用。
要调用next
,就要先调用blockdidSubscribe
。didSubscribe
要在RACMulticastConnection
对象调用方法- (void)connect;
时调用。
// 1.创建信号
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// didSubscribe什么时候调用:连接类连接的时候
// 网络请求
NSData *data;
[subscriber sendNext:data];
return nil;
}];
// 2.把信号转换成连接类
// 方法一
// RACMulticastConnection *connection = [signal publish];
// 方法二 用RACReplaySubject,就可以先发送后订阅
RACMulticastConnection *connection = [signal multicast:[RACReplaySubject subject]];
// 3.订阅连接类信号
[connection.signal subscribeNext:^(id x) {
NSLog(@"订阅者1:%@",x);
}];
[connection.signal subscribeNext:^(id x) {
NSLog(@"订阅者2:%@",x);
}];
// 4.连接
[connection connect];
复制代码
RACCommand
- 作用:监听事件 。
注意blockdidSubscribe
中,发送完成后调用了[subscriber sendCompleted]
。
// 方法一
// RACCommand:处理事件
// RACCommand:不能返回一个空的信号
// 1.创建命令
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
// input:执行[command excute:]传入的参数
// Block调用:执行[command excute:]的时候就会调用
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 发送数据
[subscriber sendNext:@"执行命令产生的数据"];
// 发送完成
[subscriber sendCompleted];
return nil;
}];
}];
// 2.执行命令
RACSignal *signal = [command execute:@1];
// 3.订阅信号
[signal subscribeNext:^(id x) {
NSLog(@"%@",x);// x = @"执行命令产生的数据"
}];
复制代码
留意方法二中的command.executionSignals
!!!方法三中也有用到。
// 方法二
// 1.创建命令
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
// input:执行[command excute:]传入的参数
// Block调用:执行[command excute:]的时候就会调用
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 发送数据
[subscriber sendNext:@"执行命令产生的数据"];
// 发送完成
[subscriber sendCompleted];
return nil;
}];
}];
// 订阅信号
// 注意:必须要在执行命令前,订阅
// executionSignals:信号源,信号中信号,signalOfSignals。信号:发送数据就是信号
[command.executionSignals subscribeNext:^(RACSignal *x) {
[x subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
}];
// 2.执行命令
[command execute:@1];
复制代码
// 方法三
// 1.创建命令
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
// input:执行[command excute:]传入的参数
// Block调用:执行[command excute:]的时候就会调用
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 发送数据
[subscriber sendNext:@"执行命令产生的数据"];
// 发送完成
[subscriber sendCompleted];
return nil;
}];
}];
// switchToLatest获取最新发送的信号,只能用于信号中信号
[command.executionSignals.switchToLatest subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
// 2.执行命令
[command execute:@1];
复制代码
说了这么多创建执行方法,那RACCommand
怎么做到监听事件呢?我们试想一下,当某个“确定按钮”点击时,网络不太好,这时候请求还未完成。那么再次点击时,期望不要发起另一次请求。这时就要用到````command.executing``,看事件的状态。
// 监听事件
// 当前命令内部发送数据完成,一定要主动发送完成
// 1.创建命令
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
// input:执行[command excute:]传入的参数
// Block调用:执行[command excute:]的时候就会调用
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 发送数据
[subscriber sendNext:@"执行命令产生的数据"];
// 发送完成
[subscriber sendCompleted];
return nil;
}];
}];
// 监听事件有没有完成
[command.executing subscribeNext:^(id x) {
if ([x boolValue] == YES) { // 当前正在执行
NSLog(@"当前正在执行");
}else{
// 执行完成/没有执行
NSLog(@"执行完成/没有执行");
}
}];
// 2.执行命令
[command execute:@1];
复制代码
RAC基本应用
虽然表面上没有出现上面提到的类,但是实质上有创建、订阅、发送信号。
- 代替代理
上面提到RACSubject
能代替代理,现在再介绍多一种更简洁的方法,其缺点是不能传值。在testView
中点击了按钮后,就会发送信号,执行blocknext
。
个人觉得,其实也就是监听着某个方法有没有被调用,不一定局限于代替代理。
// ViewController.m
@interface ViewController ()
@property (weak, nonatomic) TestView *testView;
@end
@implementation ViewController
- (void)delegate
{
// 只要传值,就必须使用RACSubject
[[self.testView rac_signalForSelector:@selector(btnClick:)] subscribeNext:^(id x) {
NSLog(@"控制器知道按钮被点击");
}];
}
@end
复制代码
- 代替KVO
跟原生的KVO比起来,简直nice得不行。
用KVO监听多个值的话,要做一堆的判断。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
}
if ([keyPath isEqualToString:@"title"]) {
}
}
复制代码
然而用RAC,因为函数式编程的原因,爽歪歪。同样的代码只要这样写。只要值改变就会发送信号,调用blocknext
。
[[self.testView rac_valuesForKeyPath:@"name" observer:nil] subscribeNext:^(id x) {
// x:修改的值
NSLog(@"%@",x);
}];
[[self.testView rac_valuesForKeyPath:@"title" observer:nil] subscribeNext:^(id x) {
// x:修改的值
NSLog(@"%@",x);
}];
复制代码
- 监听按钮点击事件
// [_btn addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
[[_btn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
NSLog(@"按钮点击了");
}];
复制代码
- 代替通知
就拿原生的键盘弹出事件来说,也能看出函数式编程的简洁。如果用原生的,监听和调用的方法分隔开又不好管理。
[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillShowNotification object:nil] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
复制代码
- 监听文本框
只要文本框一输入文字,就会发送信号。该订阅者并不会取消订阅,所以每次输入都会调用blocknext
。
[self.textField.rac_textSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
复制代码
- 综合信号结果
和GCD中的队列组相似,都是等多个任务完成后,最终执行某个任务。
用到的方法,需要注意的是
- 数组里所有信号都发送过后才会调用
Selector
。 selector
的方法里,参数 依次对应着 数组信号的发送值。
- (RACSignal *)rac_liftSelector:(SEL)selector withSignalsFromArray:(NSArray *)signals;
复制代码
例如:从两个url加载图片,最终显示在某个view上。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
RACSignal *signal1 = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
// 网络请求 根据url下载图片1
// 成功后 发送信号
NSString *firstData;
[subscriber sendNext:firstData];
return nil;
}];
RACSignal *signal2 = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
// 网络请求 根据url下载图片2
// 成功后 发送信号
NSString *secondData;
[subscriber sendNext:secondData];
return nil;
}];
// 方法的参数 依次对应着 数组信号的发送值
[self rac_liftSelector:@selector(updateWithFirstData:secondData:) withSignalsFromArray:@[signal1, signal2]];
}
- (void)updateWithFirstData:(NSString *)firstData secondData:(NSString *)secondData {
// 根据firstData 和 secondData 作出调整
}
复制代码
RAC基本应用学起来应该不难,接下来再了解一下常用宏。
RAC常用宏
RAC(<#TARGET, ...#>)
还记得上面第五点的监听文本框吗?文本框输入的值,显示到label上。
// 监听文本框内容
// [_textField.rac_textSignal subscribeNext:^(id x) {
//
// _label.text = x;
// }];
复制代码
其实能替换成一个强大的宏。
RAC(_label,text) = _textField.rac_textSignal;
复制代码
RACObserve(<#TARGET#>, <#KEYPATH#>)
为了方便上面第二点代替KVO写法。
[[self.testView rac_valuesForKeyPath:@"name" observer:nil] subscribeNext:^(id x) {
// x:修改的值
NSLog(@"%@",x);
}];
复制代码
换成
[RACObserve(self.testView, name) subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
复制代码
@weakify()
和@strongify()
- 作用:方便防止block循环引用。
__weak typeof(self) weakSelf = self;
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
__strong typeof(weakSelf) strongSelf = weakSelf;
NSLog("%@",strongSelf);
reutrn nil;
}
_signal = signal;
复制代码
以上代码只需要换成两个方便的宏。
@weakify(self);
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
@strongify(self);
NSLog("%@",self);
reutrn nil;
}
_signal = signal;
复制代码
RACTuplePack(...)
和RACTupleUnpack(...)
- 作用:包装成元组。解析元组。
// 包装元组
RACTuple *tuple = RACTuplePack(@"1",@"2");
// 解析元组
// 原来的做法
// NSString *str1 = tuple[0];
// NSString *str2 = tuple[1];
// NSLog("%@ %@", str1, str2);
// 利用宏
// 宏里面的参数,传需要解析出来的变量名。
RACTupleUnpack(NSString *str1, NSString *str2) = tuple;
NSLog("%@ %@", str1, str2);// 此时已经有变量str1 和 str2!
复制代码
ReactiveCocoa
的基础类就介绍到这,下一篇将进入开发场景。