YongSir

专业程序员伪装者

ReactiveCocoa初探

从构架说起

随着项目越来越大,再一开始简单MVC下的编程和维护也变得越来越难,直到现在还在赶工加新功能,秉承着1周一功能2周一版本的节奏,根本没有多少时间来维护和重构代码,这样下去终究会导致项目难以维护,特别是我司app中有大量的上一个大版本的代码,虽然老代码的确让新版的速度快了不少,但现在维护起来那个酸爽啊

所以越来越感觉到一个好的,简洁的,低耦合的并经过检验的构架对项目有多重要了,如果上天要再给我一次机会的话,我一定要…

看来看去瞅上了火热的MVVM,但很明显的也个大问题就是一旦M变化了,如何准确反映到UI层面,毕竟中间多了一层ViewModel,而FRP正式未解决这个问题而产生的
所以借着代码,来看看ReactiveCocoa的简单使用

ReactiveCocoa

什么诞生背景,好处弊端评价之类,现在说太空洞了,我对学习新东西一向的态度是“管他娘的,先上手再说”,所以还是按照本人一贯简单粗暴的风格,结合代码和Demo来说明

简单的Demo洗需求:实现一个登陆功能的界面,要求输入符合规定格式和字数的userName和password,这时logIn才可以点击,点击之后验证是否正确,正确就pushVC,错误就提示

这样的功能使用ReactiveCocoa下,我们是这样实现的:

初试RAC

直接上代码:

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

// signal创建和subscriber
- (void) test001 {
// signal流给订阅者-subscriber
[self.userNameTF.rac_textSignal subscribeNext:^(NSString *text) {
NSLog(@" --userName Input: %@", text);
}];

// signal流过滤,会产生过滤后的signal-filter
[[self.passwordTF.rac_textSignal filter:^BOOL(NSString *text) {
return text.length > 6;
}] subscribeNext:^(NSString *text) {
NSLog(@" --password Input: %@", text);
}];

// 对源signal流的更改,产生对不同值变化的新signal - map - signal的内容为对象
// ---NSString---[map]---> NSNumber(NSUInteger封装成对象)---
[[[self.passwordTF.rac_textSignal map:^id(NSString *text) {
return @(text.length);
}] filter:^BOOL(NSNumber *length) {
return [length integerValue] > 4;
}] subscribeNext:^(NSNumber *length) {
NSLog(@" - 当前PWD长度: %@", length);
}];
}

等下在看这段代码的解释,先看看效果:

看到了神奇的效果,你所输入的马上能够传递并被打印出来,可以完整的记录你输入的过程,有一种所见即所得的即视感
是不是很神奇?其实这就是FRP:Functional Reactive Programming[https://en.wikipedia.org/wiki/Functional_reactive_programming]的意思,函数式及时响应编程范式,今天使用的ReactiveCocoa,以及RxSwift,RxJava等等,都是基于这一思想的不同实现。

说会代码:

  • singnal : 信号,这是RAC下最基本的单元,它可以被组合,过滤,变更和传递
  • subscriber : 订阅者,这是信号的关注者,它承载着信号的最终的行为

现在来看看第一句代码到底干了些什么,userNameTF是一个UITextFiled,它是用来放用户名的,写成这样可能更加容易理解:

1
2
3
4
RACSignal *userNameSignal = self.userNameTF.rac_textSignal;
[userNameSignal subscriberNext:^(id x) {
NSLog(@" --userName Input: %@", x);
}];

首先产生了一个信号,是关于用户名的UITextFiled的text是否发生变化的信号,rac_textSignal是框架为UITextFiled提供的分类中的方法
然后将这个信号userNameSignal传递给了订阅者附带着输入的String,并且当发生signal的next时,就会去执行block中的操作,参数id x就是信号内容String *userNameText,它顺利输出,娥眉怎!

但是如果我们并不想每次都得到输入的内容,而仅仅想得到有效内容,比如我们需要一个至少6位数以上的密码呢?这就涉及到signal的过滤,就是第二段代码,它使用filter将输入的密码的String进行过滤,只有符合要求的大于6位的内容,才会被传递给subsriber触发block中的操作,这就是信号的过滤

在看最后一段,它解决了这样的问题,对于登陆LongIn按钮是否可以被点击来说,我们并不需要知道信号的String,而只是想知道什么时候可以able = YES,所以使用了map将信号转化为输入文本的长度的NSNumber,然后经filter筛选符合大于4位数的密码传给subsribter,真如你所猜想的,我们只能传递对象,信号的内容只能是对象,所以即便是bool也被转化为了NSNumber,这个过程使用’map’就完成了信号的转换

进一步,合并signal

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) test002 {

// map 去的只能说对象,所以bool要包装成NSNumber
RACSignal *validUserNameSG = [self.userNameTF.rac_textSignal map:^id(NSString *text) {
return @([self isValidUserName: text]);
}];
RACSignal *validPasswordSG = [self.passwordTF.rac_textSignal map:^id(NSString *text) {
return @([self isValidxPassword: text]);
}];


// 将validUserNameSG信号输出应用到输入框的backgroundColor上
[[validUserNameSG map:^id(NSNumber *userNameValid) {
return [userNameValid boolValue]? [UIColor clearColor] : [UIColor yellowColor];
}] subscribeNext:^(UIColor *color) {
self.userNameTF.backgroundColor = color;
}];

// 更优雅的写法,利用RAC宏 - 直接把信号对应输出到对象的属性上
RAC(self.passwordTF, backgroundColor) = [validPasswordSG map:^id(NSNumber *passwordValid) {
return [passwordValid boolValue]? [UIColor clearColor] : [UIColor yellowColor];
}];

// signal聚合 - combineLast: 可以聚合任意数量的信号 - userNameSG 和 passwordSG 聚合
RACSignal *logInActionSG = [RACSignal combineLatest:@[validUserNameSG, validPasswordSG] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid){
return @([usernameValid boolValue] && [passwordValid boolValue]);
}];

// 聚合的signal - 按钮enable属性
[logInActionSG subscribeNext:^(NSNumber *logInActive) {
self.LogIn.enabled = [logInActive boolValue];
}];
}

让我们在梳理一下需求,根据输入的内容,判断是否可以开启logIn按钮,那么至少需要2个信号,当2个信号都被认定为有效的输入时,登陆按钮才会亮起,这段代码就是完成了这样的功能,并且在用户没有有效输入时,UITextFiled的背景被设置了黄色,这样将bool信号变为了UIColor,颜色对用户的交代就更加分明了,整体的过程如果用图表示的话就是:

这其中还涉及到2个信号的合并

1
2
3
RACSignal *logInActionSG = [RACSignal combineLatest:@[validUserNameSG, validPasswordSG] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid){
return @([usernameValid boolValue] && [passwordValid boolValue]);
}];

借助combineLast[].reduce我们可以将任意的signal合并

再进一步,全部都是signal

接下来就是btn的点击,然后去请求服务器当前输入的是否是正确的,通常我们都是为btn添加事件,但别忘了这可是RAC:
在test002中,已经将用户名和密码signal聚合并绑定到LogInBtn的enable中了,但是还不够完全,事实上在响应式编程中原则尽量做到所有的一切都是signal,意味着,btn的touch事件也会被signal化 - 使用rac_signalForControlEvents,like:

1
2
3
4
5
6
- (void) test003 {
RACSignal *logInBtnClickedSG = [self.LogIn rac_signalForControlEvents:UIControlEventTouchUpInside];
[logInBtnClickedSG subscribeNext:^(id x) {
NSLog(@" - logInBtn been clicked! -");
}];
}

对signal完整的反应

现在到了去网络请求的环节了,通常异步网络回调下会有以下情形:登录成功,登录失败,其他错误
自然我们对信号的操作也要相对应,如

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) test004 {
[self test002];

// 现在,尝试有异步回调下会有以下情形:登录成功,登录失败,其他错误
// 1 创建登录操作sigal - createSignal and RACDisposable
YRServer *server = [[YRServer alloc] init];
RACSignal *logInAndCallBackSG = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[server logInWithUsername:self.userNameTF.text password:self.passwordTF.text finishedCallBack:^(BOOL isSuccess) {
[subscriber sendNext:@(isSuccess)];
[subscriber sendCompleted];
}];
return nil;
}];

// 2 登录操作 - 将按钮的登录操作事件 map为 登录操作信号
// 同时 “整流/合并信号” -Flattens/merge a stream of streams
[[[self.LogIn rac_signalForControlEvents:UIControlEventTouchUpInside] map:^id(id value) {
return logInAndCallBackSG; // 并不是普通转化,而是创建了又一个sigal,称之为[signal of signals]
}] subscribeNext:^(id x) {

// 这里需要注意:
// rac_signalForControlEvents 会将button本身 流给(next) subscribe
// 而map中,又create一个新的signal-logInAndCallBackSG
// 所以实际上在这里,得到了一个信号的信号,我称之为复合的signal
NSLog(@"得到一个信号的信号signal of signals,这里仅仅只能查看outer signal: %@", x);

// inner signal还需要自己的 subscribe
[x subscribeNext:^(id x) {

NSLog(@"inner signal - 是否登录成功了?: %@", x);
}];
}];
}
  • 使用createSignal创建了一个登陆操作的信号,对于网络操作结果
    1
    2
    3
    4
    [server logInWithUsername:self.userNameTF.text password:self.passwordTF.text finishedCallBack:^(BOOL isSuccess) {
    [subscriber sendNext:@(isSuccess)];
    [subscriber sendCompleted];
    }];

返回是否成功的bool,然后使命达成,到此为止需要关闭subscriber,这一点很重要

  • 然后在2中的btn点击后,map为刚刚进行的登陆请求操作--也就是把2个signal复合在了一起成为多层次的signal流,再强调一遍并不是普通转化,而是创建了又一个sigal,称之为[signal of signals]
    拆开来看:
    1
    2
    3
    rac_signalForControlEvents 会将button本身 流给(next) subscribe
    而map中,又create一个新的signal-logInAndCallBackSG
    所以实际上在这里,得到了一个信号的信号,我称之为复合的signal

所以在subscribeNext的block之中,如果想得到最内层的signal,就需要再加一个subscribeNext

在RAC中,这样的复合信号处理是如此普遍,以至于专门提供了处理的方法flattenMap,使用其替换上述代码2:

1
2
3
4
5
6
7
8

// 2 登录操作 - 将按钮的登录操作事件 map为 登录操作信号
// 复合信号成流[steam]
[[[self.LogIn rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^RACStream *(id value) {
return logInAndCallBackSG;
}] subscribeNext:^(id x) {
NSLog(@"inner signal - 是否登录成功了?: %@", x);
}];

就实现了完整功能

总结

综合来看,RAC彻底将传统的消息传递方式统一,不管是通知,还是UI事件,都被统一成为了signal,并且是实时响应的,这样使用MVVM,妈妈再也不用担心数据和UI状态的同步了
这里仅仅是一个小小的Demo,但是足以说服去接受RAC了,住大家玩的愉快 😄…