RAC学习笔记

体会 RAC 的魅力

大多数朋友可能和我一样,刚接触 RAC 的时候一头雾水~

so,我们通过一次体验来认识 RAC~

下面我要举个栗子:

我们对 username 和 password 进行监听,当输入任何值时候,我们打印it

PS:

  1. 栗子来源
  2. 栗子工程下载

咱们直接用起来,RAC 可以用如下代码解决:

subscribeNext–订阅者的回调 block

对usernameTextField的 text 进行监听,一旦有新值则调用 subscribeNext block

1
2
3
[self.usernameTextField.rac_textSignal subscribeNext:^(id x){
NSLog(@"%@", x);
}];

此时,结果如下:

filter–过滤

  1. 对usernameTextField的 text 进行监听
  2. 当filter 返回 YES 的时候,才调用 subscribeNext block
  3. 当filter 返回 NO 的时候,不调用 subscribeNext block
1
2
3
4
5
6
7
8
//当usernameTextField的 text 长度>3的时候,才调用subscribeNext block
[[self.usernameTextField.rac_textSignal filter:^BOOL(id value){
NSString *text = value;
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];

结果如下,从4个 q 开始输出

你会发现下面的代码和上面的代码有一样的效果,下面的代码中我将每一步分离出来了,更有效的帮助我们理解 RAC 的运行过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取一个 Signal
RACSignal *usernameSourceSignal = self.usernameTextField.rac_textSignal;
//将 usernameSourceSignal 经过 filter 过滤一下,返回一个经过 filter 包装后的崭新 Signal
//此时,只有当 usernameSourceSignal 满足 filter 的条件时,才能返回一个包装成功的崭新 Signal
RACSignal *filteredUsername = [usernameSourceSignal filter:^BOOL(id value){
NSString*text = value;
return text.length > 3;
}];
//包装后的 Signal 依然是对最初的 self.usernameTextField 进行 text 的监听
//不过此时订阅到得 text 值都是经历过 filter 筛选后能满足条件的值
[filteredUsername subscribeNext:^(id x){
NSLog(@"%@", x);
}];

map–转换传值

map从上一个next事件接收数据,通过执行block把自定义的返回值传给下一个next事件。

1
2
3
4
5
6
7
8
9
10
//监听self.usernameTextField的 text 值变化
[[[self.usernameTextField.rac_textSignal map:^id(NSString*text){
return @(text.length); //将得到的 text 新值改变为@(text.length)传给 filter
}]
filter:^BOOL(NSNumber*length){
return[length integerValue] > 3; //判断订阅到得值是否大于3来决定是否传递
}]
subscribeNext:^(id x){
NSLog(@"%@", x); //订阅到新值就打印
}];

此时,结果如下,输出的变成了数字(字符串长度),而且从4开始输出:

在上面的代码中,mapNSString为输入,取字符串的长度,返回一个NSNumber

自定义 RAC

RAC

ReactiveCocoa signal(RACSignal)发送事件流给它的subscriber。目前总共有三种类型的事件:nexterrorcompleted

一个signal在因error终止或者完成前,可以发送任意数量的next事件。

自定义RACSignal使用步骤

  1. 创建信号,didubscr这个 block 会在有订阅者订阅信号的时候调用

    + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didubscr

  2. 订阅信号,才会激活信号,netBlock这个block会在有信号发送数据时调用

    - (RACDisposable *)subscribeNext:(void (^)(id x))netBlock

  3. 发送信号

    - (void)sendNext:(idvalue

创建 RACSignal 对象

1
2
3
4
5
6
7
8
9
10
11
//创建2个RACSignal对象,根据输入框的值,转换为经过 Valid 方法验证后的值
RACSignal *validUsernameSignal =
[self.usernameTextField.rac_textSignal map:^id(NSString *text) {
return @([self isValidUsername:text]);
}];
RACSignal *validPasswordSignal =
[self.passwordTextField.rac_textSignal map:^id(NSString *text) {
return @([self isValidPassword:text]);
}];

看看2个 Valid 方法

1
2
3
4
5
6
7
8
9
//2个方法都是检验长度是否>3
- (BOOL)isValidUsername:(NSString *)username {
return username.length > 3;
}
- (BOOL)isValidPassword:(NSString *)password {
return password.length > 3;
}

RACSignal创建好了,现在需要对RACSignal值改变的情况做不同的应对

1
2
3
4
5
6
7
8
//将validPasswordSignal对象的值,经过bool验证后, 转换为UIColor对象传给下一个事件
[[validPasswordSignal map:^id(NSNumber *passwordValid){
return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
}]
subscribeNext:^(UIColor *color){ //将受到的 UIColor 对象直接设置为背景色
self.passwordTextField.backgroundColor = color;
}];

执行结果如下:

PS: 不要使用上面这段代码,下面有一种更好的写法!上面这段代码只是帮助我们理解过程

1
2
3
4
5
6
7
8
9
RAC(self.passwordTextField, backgroundColor) =
[validPasswordSignal map:^id(NSNumber *passwordValid){
return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
}];
RAC(self.usernameTextField, backgroundColor) =
[validUsernameSignal map:^id(NSNumber *passwordValid){
return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
}];

上面浓缩代码的解析如下:

RAC宏允许直接把信号的输出应用到对象的属性上。

RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。

combineLatest–聚合

现在的代码中已经有可以产生用户名和密码输入框是否有效的信号了——validUsernameSignalvalidPasswordSignal了。现在需要做的就是聚合这两个信号来决定登录按钮是否可用。

1
2
3
4
5
RACSignal *signUpActiveSignal =
[RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
reduce:^id(NSNumber*usernameValid, NSNumber *passwordValid){
return @([usernameValid boolValue]&&[passwordValid boolValue]);
}];

上面的代码使用combineLatest:reduce:方法把validUsernameSignalvalidPasswordSignal产生的最新的值聚合在一起,并生成一个新的信号。每次这两个源信号的任何一个产生新值时,reduce block都会执行,block的返回值会发给下一个信号。

1
2
3
[signUpActiveSignal subscribeNext:^(NSNumber*signupActive){
self.signInButton.enabled =[signupActive boolValue];
}];

运行结果:

Button类 RAC 使用

给button的 TouchUpInside 事件绑定一个信号,给这个信号添加了订阅,每当这个事件触发的时候,调用订阅的 block 方法

1
2
3
[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
NSLog(@"button clicked");
}];

信号内信号- map 与 flattenMap

下面这个是给 button 的点击事件,添加一个信号,用 map 转换后,传给下一个订阅者打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- (void)test {
[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside]
map:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(id x){
NSLog(@"Sign in result: %@", x);
}];
}
- (RACSignal *)signInSignal {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber){
[self.signInService signInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text
complete:^(BOOL success){
[subscriber sendNext:@(success)];
[subscriber sendCompleted];
}];
return nil;
}];
}
- (void)signInWithUsername:(NSString *)username password:(NSString *)password
complete:(RWSignInResponse)completeBlock {
double delayInSeconds = 0.0;
dispatch_time_t popTime =
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
BOOL success = [username isEqualToString:@"user"] && [password isEqualToString:@"password"];
completeBlock(success);
});
}

这段代码我们想要执行的效果是监控到UIControlEventTouchUpInside后判断输入的账号密码是否正确,并且将判断结果(BOOL)打印出来

实际结果:

这是什么鬼?说好的BOOL呢?

分析一下,我们将 上面第一个方法分解后,如下

1
2
3
4
5
6
7
8
9
10
RACSignal *singnal1 =
[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside];
RACSignal *singnal2 = [singnal1 map:^id(id x){
return[self signInSignal];
}];
[singnal2 subscribeNext:^(id x){
NSLog(@"Sign in result: %@", x);
}];

可以看出每一步传递的都是一个 RACSingnal信号,其中 map 调用了- (RACSignal *)signInSignal是要把信号传递的参数改变了.

但是- (RACSignal *)signInSignal内部自己又创建了一个信号,并且返回了这个信号,所以我们在 NSLog打印的时候是一个乱七八糟鬼~代码如下

其实我们想要的是- (RACSignal *)signInSignal内部[subscriber sendNext:@(success)];这个传递的数据.

RAC封装了另外一种 flattenMap 方法来代替 map处理这种信号中含有信号的情况,会自动转换

所以,遇到信号中包含另一个信号,并且想将sendNext:发送的数据传到外部信号中,可以用flattenMap代替map

1
2
3
4
5
6
7
8
9
10
11
[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside]
flattenMap:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(NSNumber*signedIn){
BOOL success =[signedIn boolValue];
self.signInFailureText.hidden = success;
if(success){
[self performSegueWithIdentifier:@"signInSuccess" sender:self];
}
}];

subscribeNext:block从登录信号中取得结果,相应地更新signInFailureText是否可见。如果登录成功执行导航跳转。

doNext– 附加操作

当我们想在每一次按钮被点击,发送事件的过程中,做一点与事件本身并无联系的事儿,可以用 doNext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x){
self.signInButton.enabled =NO;
self.signInFailureText.hidden =YES;
}]
flattenMap:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(NSNumber*signedIn){
self.signInButton.enabled =YES;
BOOL success =[signedIn boolValue];
self.signInFailureText.hidden = success;
if(success){
[self performSegueWithIdentifier:@"signInSuccess" sender:self];
}
}];

上面的代码将每一次按钮点击后按钮本身置为不可点状态,并且隐藏错误文本,当校验失败时再重新恢复按钮为可点击状态,并且显示错误文本~

最终工程

上面栗子最终工程下载点我

RAC Tips

RAC 事件传递的值是什么?

可以看到下面的打印 X 为一个 button, NSLog内的却是0

0来源于图1 block中传递的 BOOL, 那么 button 对象来源于哪儿?

从 button 订阅点击事件的信号中可以看到,调用 sendNext: 的对象是 subscriber,但是:内传递的参数是 button 自己,所以外面打印的x 就是 button

so. 信号订阅者接收到得值是什么,取决于上一个信号 sendNext:方法里发送的是什么

RAC使后感

  1. RAC 函数调用,环环相扣~e.g.调用形式:[[[XXX map] filter] subscribeNext:Block]
  2. 每次调用方法的实例对象都是 RACSignal 对象
  3. RACSignal对象直接传递的值也是一个对象
  4. 代码中没有用来表示两个输入框有效状态的私有属性。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。
  5. 下一个订阅者收到的传递参数,取决于上一个RACSignal对象 sendNext 的值是什么

来源:

1.raywenderlich’s Blog


本站文章若无特别说明,均为原创文章,转载请注明地址: https://kevinmky.github.io