写在前面
作为一名快具备两年多工作经验的IOS开发者的我,在使用了oc 、swift、storyboard开发之后,难免觉得对单纯开发有点乏味,所以在以后的项目都需要有所改变。swift4.0在今年的九月份就要出正式版了,我想以后我可能几乎不会使用oc 来开发项目了,所以这可能是我最后一个oc项目了,额,我会以这样的方式来开心下----使用MVVC模式,并加上ReactiveCocoa响应式编程,这样可以增强对RxSwift的理解。
温馨提示:RAC的编程方式虽然已经被很多人接纳了,但是在做项目的时候也要根据能力、项目的松紧程度、后期的维护来选择。当这样的一个项目交接给别人来维护时,对后期的维护人员的选择是有一定的难度的,每个人的编程方式是不一样的,而对RAC不熟的童鞋来说,入手是件比较辛苦的事情。
进入正题
RAC入门
作为一个iOS开发者,你写的每一行代码几乎都是在响应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。
RAC图文表示
RAC可以简单理解成一下图
http://www.jianshu.com/p/7fbd3453e0ee
比较给力的教程参考文档
https://www.raywenderlich.com/62699/reactivecocoa-tutorial-pt1
感谢很多大神翻译的文章可以让更多的人快速入门ReactiveCocoa,以下是我的理解和补充
RAC初体验
- 醉醉醉基本的功能:
IOS的事件处理,比如action、delegate、KVO、callback等,为事件定义了一个标准接口。
一个demo展示demo
action.png RAC.png谈论:
RAC将action方法转成了block的形式
似不似觉得代码瞬间变得高大上了。并且这只是RAC的很小的一个部分。RAC给几乎所有的UI控件添加了适用于ARC响应式编程的方法和属性:如下:
RAC针对UI控件添加方法和属性.png
- 可以发现每一个UI控件都添加了RACSignal属性,控件的使用都是基于RACSignal属性的,都是由RACSignal发送事件流给它的subscriber。
- ReactiveCocoa框架使用category来为很多基本UIKit控件添加signal(RACSignal属性)。这样你就能给控件添加订阅了,text field的rac_textSignal就是这么来的。
- UI方面做了需要的封装成block的操作。
可以理解成这样
(1) ReactiveCocoa signal(RACSignal)发送事件流给它的subscriber。目前总共有三种类型的事件:next、error、completed。一个signal在因error终止或者完成前,可以发送任意数量的next事件。在这里,我们将会关注next事件。以后将会学习error和completed事件。
(2) RACSignal有很多方法可以来订阅不同的事件类型。每个方法都需要至少一个block,当事件发生时就会执行block中的逻辑。在上面的例子中可以看到每次next事件发生时,subscribeNext:方法提供的block都会执行。
- 添加过滤(filter过滤器)
很多情况下,我们会要求当输入的字符长度超过n的时候,改变下背景颜色。
// 只关心超过3个字符长度的用户名
[[self.textField.rac_textSignal filter:^BOOL(id value) {
NSString *text = value;
self.textField.backgroundColor = [UIColor blueColor];
return text.length > 3;
}] subscribeNext:^(id x) {
self.textField.backgroundColor = [UIColor redColor];
NSLog(@"%@",x);
}];
也可以写成
// 其他的写法
// filter操作的输出也是RACSignal
// 获取textField的信号
RACSignal *textFieldSoureSignal = self.textField.rac_textSignal;
// 获取textField的信号的过滤信号
RACSignal *filteredTextField = [textFieldSoureSignal filter:^BOOL(id value) {
// 过滤条件
NSString *text = value;
return text.length > 3;
}];
// 过滤信号的操作
[filteredTextField subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
RACSignal的每个操作都会返回一个RACsignal,这在术语上叫做连贯接口(fluent interface)。这个功能可以让你直接构建管道,而不用每一步都使用本地变量。
注意:ReactiveCocoa大量使用block。如果你是block新手,你可能想看看Apple官方的block编程指南。如果你熟悉block,但是觉得block的语法有些奇怪和难记,你可能会想看看这个有趣又实用的网页f*****gblocksyntax.com。
-
事件类型
刚刚在代码中使用的filter、Next都是事件类型。现在添加map事件。
// 添加map操作
[[[self.textField.rac_textSignal
map:^id(NSString *text) {
return @(text.length);
}]
filter:^BOOL(NSNumber *length) {
return [length integerValue] > 3;
}]
subscribeNext:^(NSString *x) {
NSLog(@"%@",x);
}];
log输出:
map操作log输出.png
map操作将修改事件的数据.png新加的map操作通过block改变了事件的数据。map从上一个next事件接收数据,通过执行block把返回值传给下一个next事件。在上面的代码中,map以NSString为输入,取字符串的长度,返回一个NSNumber,在接下来的管道中如果不修改类型都是以NSNumber传递下去。
-
创建有效状态信号
创建信号,标识输入框的输入内容是否有效。
RACSignal *validTextFieldSignal = [self.textField.rac_textSignal map:^id(NSString *text) {
return @([self isValidTextField:text]);
}];
上面的代码对每个输入框的rac_textSignal应用了一个map转换。输出是一个用NSNumber封装的布尔值。
下一步是转换这些信号,从而能为输入框设置不同的背景颜色。基本上就是,你订阅这些信号,然后用接收到的值来更新输入框的背景颜色。
[[validTextFieldSignal map:^id(NSNumber *num) {
return [num boolValue] ? [UIColor redColor] : [UIColor blueColor];
}]
subscribeNext:^(UIColor *color) {
self.textField.backgroundColor = color;
}];
推荐下面的宏的写法:
// RAC宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。
RAC(self.textField, backgroundColor) = [validTextFieldSignal map:^id(NSNumber *num) {
return [num boolValue] ? [UIColor redColor] : [UIColor blueColor];
}];
RAC宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。
-
聚合信号
举个例子,当用户名、密码输入框的输入内容都有效时登录按钮才能点击。现在要把这里改成响应式的。
// 输入框
RACSignal *validTextFieldSignal = [self.textField.rac_textSignal map:^id(NSString *text) {
return @([self isValidTextField:text]);
}];
// 输入框1
RACSignal *validTextFieldSignal1 = [self.textField1.rac_textSignal map:^id(NSString *text) {
return @([self isValidTextField:text]);
}];
//合并信号
//每当validTextFieldSignal和validTextFieldSignal1变化的时候,都会产生一个新的信号return出来
RACSignal *signupActiveSignal = [RACSignal combineLatest:@[validTextFieldSignal, validTextFieldSignal1] reduce:^id(NSNumber *validTextField, NSNumber *validTextField1){
return @([validTextField boolValue] && [validTextField1 boolValue]);
}];
// 新的信号变化,就会进行subscribeNext操作
[signupActiveSignal subscribeNext:^(NSNumber *signupActive) {
self.loginBtn.enabled = [signupActive boolValue];
}];
RAC合并概念.png
上图展示了一些重要的概念,你可以使用ReactiveCocoa来完成一些重量级的任务。
- 分割——信号可以有很多subscriber,也就是作为很多后续步骤的源。注意上图中那个用来表示用户名和密码有效性的布尔信号,它被分割成多个,用于不同的地方。
- 聚合——多个信号可以聚合成一个新的信号,在上面的例子中,两个布尔信号聚合成了一个。实际上你可以聚合并产生任何类型的信号。
代码中没有用来表示两个输入框有效状态的私有属性。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。
-
创建信号
现在就可以点击登录按钮了,按钮按下的处理ReactiveCocoa提供了rac_signalForControlEvents方法可以实现。
// 点击登录
[[self.loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *loginBtn) {
NSLog(@"%@",loginBtn);
}];
现在我们可以将登录的异步API(封装好的网络请求)放在回调方法中执行,这样就可以完成登录的要求了。
- 思考一下,如果把所谓的登录的异步API用信号的方式来表示,那么代码会是什么样子?
我们可以使用map操作将登录的异步API转成获取到了登录结果的“信号”
(可以想成经过登录操作后异步回调的结果,可能就是一个布尔值),那就可以写成事件流的形式。
创建获取到了登录结果的“信号”
// 创建登录信号
- (RACSignal *)signInSignal {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[[RWDummySignInService alloc] init] signInWithUsername:self.textField.text
password:self.textField.text
complete:^(BOOL success) {
// 发送登录的结果
[subscriber sendNext:@(success)];
[subscriber sendCompleted];
}];
return nil;
}];
}
那么代码就可以写成这样:
// (将按钮的点击信号 转成 登录信号)
[[[self.loginBtn
rac_signalForControlEvents:UIControlEventTouchUpInside]
// 将按钮的点击信号-->登录信号
map:^id(id value) {
// 后续传递的都是登录信号
return [self signInSignal];
}] subscribeNext:^(id x) {
// next操作中输出的是登录信号(而不是登录的结果)
NSLog(@"%@",x);
}];
map方法.png上面的代码使用map方法,把按钮点击信号转换成了登录信号,subscriber输出的log。输出的是一个信号对象(这是什么鬼)
上面问题的解决方法,有时候叫做信号中的信号,换句话说就是一个外部信号里面还有一个内部信号。你可以在外部信号的subscribeNext:block里订阅内部信号。不过这样嵌套太混乱啦,还好ReactiveCocoa已经解决了这个问题。(其实我并不知道应该怎么写)
- 信号中的信号
解决的方法很简单,只需要把map操作改成flattenMap就可以了:
// (将按钮的点击信号 转成 登录信号)
[[[self.loginBtn
rac_signalForControlEvents:UIControlEventTouchUpInside]
// 将按钮的点击信号-->登录信号
flattenMap:^id(id value) {
// 后续传递的都是登录信号
return [self signInSignal];
}]
subscribeNext:^(id x) {
// next操作中输出的是登录结果
NSLog(@"%@",x);
}];
可以查看下内部源码就会发现map操作是基于flattenMap
map操作.png
-
添加附加操作(Adding side-effects)
你注意到这个应用现在有一些用户体验上的小问题了吗?当登录service正在校验用户名和密码时,登录按钮应该是不可点击的。这会防止用户多次执行登录操作。还有,如果登录失败了,用户再次尝试登录时,应该隐藏错误信息。
doNext:是直接跟在按钮点击事件的后面。而且doNext: block并没有返回值。因为它是附加操作,并不改变事件本身。
之前的管道图就更新成下面这样的:
注意:在异步操作执行的过程中禁用按钮是一个常见的问题,ReactiveCocoa也能很好的解决。RACCommand就包含这个概念,它有一个enabled信号,能让你把按钮的enabled属性和信号绑定起来。你也许想试试这个类。
网友评论