前言
在上一篇ReactiveCocoa基础类学习笔记中,笔者从初学者的角度把RAC基础类梳理了一遍,但学完还是不知道怎么应用。这篇将带大家进一步探索RAC各种理论和实战应用。
RAC 操作
RAC实在有太多的方法了,同样的问题能采取多种方法实现。笔者在本文只选取了基本且能 实现其他方法效果 的方法。对RAC更多方法有兴趣的可以查看最快让你上手ReactiveCocoa之进阶篇。
某种场景可用多种方式实现。开发中喜欢哪种就用哪种吧,没必要全部记住,太多了。汇总成一个表。
imageRAC开发中核心方法——bind(绑定)
在开发中,很少直接用到bind
,因为RAC已经封装了很多方法。
虽然很少用,但理解了执行顺序后,会对理解后面有帮助。
// 1.创建源信号
RACSubject *subject = [RACSubject subject];
// 2.绑定信号
RACSignal *bindSignal = [subject bind:^RACStreamBindBlock{
// 3.1
// block调用时刻:只要绑定信号被订阅就会调用
return ^RACSignal *(id value, BOOL *stop){
// 4.1
// block调用:只要源信号发送数据,就会调用
// block作用:处理源信号内容
// value:源信号发送的内容
// 处理源信号内容,加点东西
value = [NSString stringWithFormat:@"加点东西%@",value];
// 返回信号,不能传nil,返回空信号[mRACSignal empty]
return [RACReturnSignal return:value];
};
}];
// 3.订阅绑定信号
[bindSignal subscribeNext:^(id x) {
// 5.
// blcok:当return [RACReturnSignal return:value],就会调用这个Block
NSLog(@"接收到绑定信号处理完的信号%@",x);
}];
// 4.源信号发送数据
[subject sendNext:@"123"];
信号的映射和过滤
映射:把源信号内容映射成新的内容
两种方式:map
和flattenMap
。
区别:map
用于基本的信号,flattenMap
常用于信号中信号。
// map用法
// 创建信号
RACSubject *subject = [RACSubject subject];
// 绑定信号
RACSignal *bindSignal = [subject map:^id(id value) {
// 返回的类型,就是你需要映射的值
return [NSString stringWithFormat:@"修改内容:%@",value];
}];
// 订阅绑定信号
[bindSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
[subject sendNext:@"233"];
// fluttenMap用法
RACSubject *signalOfsignals = [RACSubject subject];
RACSubject *signal = [RACSubject subject];
// 订阅信号
// 注释部分为分部,方便新手理解
// [signalOfsignals subscribeNext:^(RACSignal *x) {
//
// [x subscribeNext:^(id x) {
// NSLog(@"%@",x);
// }];
//
// }];
// RACSignal *bindSignal = [signalOfsignals flattenMap:^RACStream *(id value) {
// // value:源信号发送内容
// return value;
// }];
//
// [bindSignal subscribeNext:^(id x) {
//
// NSLog(@"%@",x);
// }];
// 此处连起来处理,对应着注释部分
[[signalOfsignals flattenMap:^RACStream *(id value) {
return value;
}] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
// 发送信号
[signalOfsignals sendNext:signal];
[signal sendNext:@"233"];
过滤:获取满足条件的信号。
试想这种场景,文本框长度大于5时,才让按钮可点击。这时就要把文本框的信号过滤出来,长度超过5的信号,再订阅。
[[self.textField.rac_textSignal filter:^BOOL(id value) {
// value:源信号的内容
return [value length] > 5;
}] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
组合信号
组合(concat):信号按顺序连接。
比如先执行完signalA
的didSubscribe
,再执行signalB
的didSubscribe
。
虽然可以在signalA
的didSubscribe
中按顺序拼上signalB
的didiSubscribe
。但用这种方法能聚合起来。
// 组合
// 创建信号A
RACSignal *siganlA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 网络请求 根据url下载图片1
// 成功后 发送信号
[subscriber sendNext:@"信号A"];
[subscriber sendCompleted];
return nil;
}];
RACSignal *siganlB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 网络请求 根据url下载图片2
// 成功后 发送信号
[subscriber sendNext:@"信号B"];
return nil;
}];
// concat:按顺序去连接
// 注意:concat,第一个信号必须要调用sendCompleted
// 创建组合信号
RACSignal *concatSignal = [siganlA concat:siganlB];
// 订阅组合信号
[concatSignal subscribeNext:^(id x) {
// 既能拿到A信号的值,又能拿到B信号的值
NSLog(@"%@",x);
}];
image
当concatSignal
被订阅,就会按顺序订阅被组合信号。
注意的是,signalA
要[subscriber sendCompleted]
。
signalA
中[subscriber sendNext]
将会执行concatSignal
的next
。
压缩信号
zipWith(压缩):两个信号都执行过后再执行某个任务。和上一篇文章提到的综合信号结果rac_liftSelector
一样的效果。
// 创建信号A
RACSubject *signalA = [RACSubject subject];
// 创建信号B
RACSubject *signalB = [RACSubject subject];
// 压缩成一个信号
// zipWith:当一个界面多个请求的时候,要等所有请求完成才能更新UI
// zipWith:等所有信号都发送内容的时候才会调用
RACSignal *zipSignal = [signalA zipWith:signalB];
// 订阅信号
[zipSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
// 发送信号
[signalB sendNext:@2];
[signalA sendNext:@1];
合并信号并聚合元组
开发中常用。其和压缩信号的区别是:压缩信号要两个信号都执行过,组合信号不用。
组合两个文本框的信号,最终让登录按钮是否可交互。
// 组合
// 组合哪些信号
// reduce:聚合
// reduceBlock参数:根组合的信号有关,一一对应
RACSignal *comineSiganl = [RACSignal combineLatest:@[_accountFiled.rac_textSignal,_pwdField.rac_textSignal] reduce:^id(NSString *account,NSString *pwd){
// block:只要源信号发送内容就会调用,组合成新一个值
NSLog(@"%@ %@",account,pwd);
// 聚合的值就是组合信号的内容
return @(account.length && pwd.length);
}];
// 订阅组合信号
// [comineSiganl subscribeNext:^(id x) {
// _loginBtn.enabled = [x boolValue];
// }];
RAC(_loginBtn,enabled) = comineSiganl;
RAC + MVVM 实战
如果你对MVVM不了解,可以看这篇文章iOS 关于MVVM With ReactiveCocoa设计模式的那些事
这里简单说一下MVVM。
- M(模型):保存视图数据。
- V(视图+控制器):展示内容+如何展示。
- VM(视图模型):处理展示的业务逻辑,包括按钮点击,数据的请求、解析等。
- 每一个控制器都对应一个VM。
下面将一步步走进MVVM的世界。
笔者学的时候例子是登录界面,写出来加深一下理解。
要做到 账号和密码文本框内容符合条件后,登录按钮才能点击。
还不理解MVVM的你可能在viewDidLoad
写出以下代码。
// 1.处理文本框业务逻辑
RACSignal *loginEnableSiganl = [RACSignal combineLatest:@[_accountFiled.rac_textSignal,_pwdField.rac_textSignal] reduce:^id(NSString *account,NSString *pwd){
return @(account.length && pwd.length >= 8);
}];
// 设置按钮能否点击
RAC(_loginBtn,enabled) = loginEnableSiganl;
// 创建登录命令
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 发送登录请求
// 成功后发送数据
[subscriber sendNext:@"请求登录的数据"];
[subscriber sendCompleted];
});
return nil;
}];
}];
// 订阅命令中信号源
[command.executionSignals.switchToLatest subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
// 监听命令执行过程
[[command.executing skip:1] subscribeNext:^(id x) {
if ([x boolValue] == YES) {
// 正在执行
NSLog(@"正在执行");
// 显示蒙版
[MBProgressHUD showMessage:@"正在登录..."];
}else{
// 执行完成
// 隐藏蒙版
[MBProgressHUD hideHUD];
NSLog(@"执行完成");
}
}];
// 监听登录按钮点击
[[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
NSLog(@"点击登录按钮");
// 处理登录事件
[command execute:nil];
}];
能理解以上代码说明你掌握了RAC的基础用法。但是要应用到MVVM架构就要转到重点VM
中了。
这时候每个控制器就要创建一个LoginViewModel
,在VM.h
中暴露出信号接口用于绑定。
@interface LoginViewModel : NSObject
@property (nonatomic, strong, readonly) RACSignal *loginEnableSiganl;
@property (nonatomic, strong, readonly) RACCommand *loginCommand;
@end
但发现loginCommand
可以直接放进去;loginEnableSiganl
要拿到文本框的值,然而VM中是不能引用UI的。
于是在VM.h
中加上了账号和密码。
@interface LoginViewModel : NSObject
@property (nonatomic, strong) NSString *account;
@property (nonatomic, strong) NSString *pwd;
@property (nonatomic, strong, readonly) RACSignal *loginEnableSiganl;
@property (nonatomic, strong, readonly) RACCommand *loginCommand;
@end
然后在控制器中给该属性绑定信号。做到实时对应文本框内容。并且把按钮可否点击绑定到信号中。
-(void)viewDidLoad {
RAC(self.loginVM, account) = _accountField.rac_textSignal;
RAC(self.loginVM, pwd) = _pwdField.rac_textSignal;
RAC(_loginBtn, enabled) = self.loginVM.loginEnableSignal;
[[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
// 处理登录事件
[self.loginVM.loginCommand execute:nil];
}];
}
这时候在VM中就能处理登录信号了。当然loginCommand
可以直接放进去。所以VM.m
这时将会变成这样。
//LoginViewModel.m
@implementation LoginViewModel
- (instancetype)init
{
if (self = [super init]) {
[self setUp];
}
return self;
}
- (void)setUp
{
_loginEnableSiganl = [RACSignal combineLatest:@[RACObserve(self, account),RACObserve(self, pwd)] reduce:^id(NSString *account,NSString *pwd){
return @(account.length && pwd.length >= 8);
}];
_loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// 发送登录请求
// 成功后发送数据
[subscriber sendNext:@"请求登录的数据"];
[subscriber sendCompleted];
});
return nil;
}];
}];
[_loginCommand.executionSignals.switchToLatest subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
[[_loginCommand.executing skip:1] subscribeNext:^(id x) {
if ([x boolValue] == YES) {
// 正在执行
NSLog(@"正在执行");
// 显示蒙版
[MBProgressHUD showMessage:@"正在登录.."];
}else{
// 执行完成
// 隐藏蒙版
[MBProgressHUD hideHUD];
NSLog(@"执行完成");
}
}];
}
@end
在本文开头引用的进阶篇内还有结合网络的实战,本文就不再阐述了。
最后来总结一下怎么用RAC + MVVM。
- 为VM准备好view需要的数据和命令。写好初始化方法。
- 在控制器中,写好view。
- 在控制器中,将信号绑定好。
最后,相信学完基础类和基础应用,这篇文章能帮你更好地理解。iOS 关于MVVM With ReactiveCocoa设计模式的那些事。
网友评论