从构架说起
随着项目越来越大,再一开始简单MVC下的编程和维护也变得越来越难,直到现在还在赶工加新功能,秉承着1周一功能2周一版本的节奏,根本没有多少时间来维护和重构代码,这样下去终究会导致项目难以维护,特别是我司app中有大量的上一个大版本的代码,虽然老代码的确让新版的速度快了不少,但现在维护起来那个酸爽啊
所以越来越感觉到一个好的,简洁的,低耦合的并经过检验的构架对项目有多重要了,如果上天要再给我一次机会的话,我一定要…
看来看去瞅上了火热的MVVM,但很明显的也个大问题就是一旦M变化了,如何准确反映到UI层面,毕竟中间多了一层ViewModel,而FRP正式未解决这个问题而产生的
所以借着代码,来看看ReactiveCocoa
的简单使用
ReactiveCocoa
什么诞生背景,好处弊端评价之类,现在说太空洞了,我对学习新东西一向的态度是“管他娘的,先上手再说”,所以还是按照本人一贯简单粗暴的风格,结合代码和Demo来说明
简单的Demo洗需求:实现一个登陆功能的界面,要求输入符合规定格式和字数的userName和password,这时logIn才可以点击,点击之后验证是否正确,正确就pushVC,错误就提示
这样的功能使用ReactiveCocoa下,我们是这样实现的:
初试RAC
直接上代码:
1 |
|
等下在看这段代码的解释,先看看效果:
看到了神奇的效果,你所输入的马上能够传递并被打印出来,可以完整的记录你输入的过程,有一种所见即所得
的即视感
是不是很神奇?其实这就是FRP:Functional Reactive Programming
[https://en.wikipedia.org/wiki/Functional_reactive_programming]的意思,函数式及时响应编程范式,今天使用的ReactiveCocoa,以及RxSwift,RxJava等等,都是基于这一思想的不同实现。
说会代码:
- singnal : 信号,这是RAC下最基本的单元,它可以被组合,过滤,变更和传递
- subscriber : 订阅者,这是信号的关注者,它承载着信号的最终的行为
现在来看看第一句代码到底干了些什么,userNameTF是一个UITextFiled
,它是用来放用户名的,写成这样可能更加容易理解:
1 | RACSignal *userNameSignal = self.userNameTF.rac_textSignal; |
首先产生了一个信号,是关于用户名的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 | - (void) test002 { |
让我们在梳理一下需求,根据输入的内容,判断是否可以开启logIn按钮,那么至少需要2个信号,当2个信号都被认定为有效的输入时,登陆按钮才会亮起,这段代码就是完成了这样的功能,并且在用户没有有效输入时,UITextFiled
的背景被设置了黄色,这样将bool信号变为了UIColor,颜色对用户的交代就更加分明了,整体的过程如果用图表示的话就是:
这其中还涉及到2个信号的合并
1 | RACSignal *logInActionSG = [RACSignal combineLatest:@[validUserNameSG, validPasswordSG] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid){ |
借助combineLast[].reduce
我们可以将任意的signal合并
再进一步,全部都是signal
接下来就是btn的点击,然后去请求服务器当前输入的是否是正确的,通常我们都是为btn添加事件,但别忘了这可是RAC:
在test002中,已经将用户名和密码signal聚合并绑定到LogInBtn的enable中了,但是还不够完全,事实上在响应式编程中原则尽量做到所有的一切都是signal
,意味着,btn的touch事件也会被signal化 - 使用rac_signalForControlEvents
,like:
1 | - (void) test003 { |
对signal完整的反应
现在到了去网络请求的环节了,通常异步网络回调下会有以下情形:登录成功,登录失败,其他错误
自然我们对信号的操作也要相对应,如
1 | - (void) test004 { |
- 使用
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
3rac_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了,住大家玩的愉快 😄…