以技术之名周报06#| ReactiveCocoa入门篇| 2020-05-10

Part-01 背景

作为一个iOS开发者,写的每一行代码都是对事件的反馈,像Button点击、网络请求、属性改变(KVO)、用户位置改变等。但是这些事件的处理采用的是Actions、delegate、KVO、回调等不同的方式。ReactiveCocoa针对不同的事件定义了的标准接口,这样不同的事件可以更容易链式调用、过滤、组合。

ReactiveCocoa组合了一对编程范式:函数式编程:使用更高级别的函数,该函数用其他函数作为他的参数。响应式编程:一种面向数据流和变化传播的声明式编程范式。所以,ReactiveCocoa也被称之为函数响应式编程框架。

ReactiveCocoa教程马上开始,接下来的教程侧重点在实用价值,所以给出的是实操而不是长篇大论。

Part-02 实践

ReactivePlayground

在接下来的整个ReactiveCocoa教程中,我们将通过ReactivePlayground这个应用来逐步引入响应式编程。下载ReactivePlayground工程,确保可以正常的build和run.

ReactivePlaygroun是一款非常简单的应用,主要就是给用户展示了一个登录的页面。输入用户的账号和密码,验证通过之后,进入到一个有一只可爱猫咪的页面。

Alt text

打开工程,找到 RWViewController.m文件,你花费多久的时间可以找到Sign In Button变成enabing状态所需要的条件?展示/隐藏 signInFaulure Label的规则是什么? 也许,你花费两三分钟的时间就可以回答这些问题,但是当你面对更加复杂的项目的时候,你分析同样的问题可能就会花费相当长的时间。

这就是ReactiveCocoa的优势所在,ReactiveCocoa能够让应用程序的基本意图变得更加清晰。那让我们开始我们的工作吧!!!

添加ReactiveCocoa框架

最方便的方式就是通过CocoaPods.的方式导入框架。如果在此之前你没有使用过CocoaPods.,可以按照本网站的CocoaPods 入门教程进行操作,或者至少通过该教程的初始步骤进行操作,以便安装必备的组件。

如果因为某些原因你不想引入CocoaPods,你仍旧可以通过其他的方式使用ReactiveCocoa。可以参考Github上面的引入ReactiveCocoa文档,一步步操作。

打开终端,进入到下载工程的根目录,执行下面的指令创建Podfile文件;

1
2
3
touch Podfile
open -e Podfile

用文本编辑器打开Podfile文件,复制下面的代码到里面

1
2
3
platform :ios, '7.0'
pod 'ReactiveCocoa', '2.1.8'

这两句代码的意思是设置iOS平台且最小支持的版本是7.0、添加ReactiveCocoa框架作为依赖。保存文件,继续在终端执行下面的命令:

1
2
pod install

你会看到终端打印出类似下面的内容:

1
2
3
4
5
6
7
8
Analyzing dependencies
Downloading dependencies
Installing ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project

[!] From now on use `RWReactivePlayground.xcworkspace`.

这意味着ReactiveCocoa framework已经被下载下来了,CocoaPods创建一个Xcode workspace文件将应用和framework整合起来了

Alt text

开始实施

正如上面介绍中提到的,ReacticeCocoa为应用程序中发生的事件流提供了一个标准的接口。这种接口在ReactiveCocoa中称为信号(Signal),通过RACSignal类来表示。

打开 RWViewController.m文件,引入ReactiveCocoa头文件

1
2
#import <ReactiveCocoa/ReactiveCocoa.h>

暂时先不用替换任何现有代码,只需要做一些操作即可。添加下面的代码到viewDidload方法:

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

运行应用程序,在username textField中输入文字,查看控制台输出的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2013-12-24 14:48:50.359 RWReactivePlayground[9193:a0b] i
2013-12-24 14:48:50.436 RWReactivePlayground[9193:a0b] is
2013-12-24 14:48:50.541 RWReactivePlayground[9193:a0b] is
2013-12-24 14:48:50.695 RWReactivePlayground[9193:a0b] is t
2013-12-24 14:48:50.831 RWReactivePlayground[9193:a0b] is th
2013-12-24 14:48:50.878 RWReactivePlayground[9193:a0b] is thi
2013-12-24 14:48:50.901 RWReactivePlayground[9193:a0b] is this
2013-12-24 14:48:51.009 RWReactivePlayground[9193:a0b] is this
2013-12-24 14:48:51.142 RWReactivePlayground[9193:a0b] is this m
2013-12-24 14:48:51.236 RWReactivePlayground[9193:a0b] is this ma
2013-12-24 14:48:51.335 RWReactivePlayground[9193:a0b] is this mag
2013-12-24 14:48:51.439 RWReactivePlayground[9193:a0b] is this magi
2013-12-24 14:48:51.535 RWReactivePlayground[9193:a0b] is this magic
2013-12-24 14:48:51.774 RWReactivePlayground[9193:a0b] is this magic?

可以看到,每当改变textfield内容的时候,block中的代码都会执行。这里既没有target-action、也没有代理方法,仅仅用到了signals和block,令人激动万分!

ReactiveCocoa的 信号(通过RACSignal表示)发送事件流给它的订阅者。主要是有三种类型的事件:nexterrorcompleted. 信号会因为error或者complete结束,但在结束之前可以发送任意数量的next事件。

RACSignal有多种方法用于订阅不同的事件类型,每种方法都一个或者多个block,事件发生的时候可以用来执行你想要的逻辑。比如:subscribeNext:方法就提供了这样一个block,每当next事件发生的时候,就会执行该block;

ReactiveCocoa框架通过类别给标准的UIKit控件添加信号,因此你可以订阅这些控件的事件。这就是你可以在textfield上使用rac_textSignal属性的原因。

ReactiveCocoa有大量的可以用来操纵事件流的操作符。比如:你只对长度超过三个字符的用户名感兴趣,那么就可以使用 filter操作符。将viewDidload中添加的代码更新为下面的代码:

1
2
3
4
5
6
7
8
9
[[self.usernameTextField.rac_textSignal
filter:^BOOL(id value) {
NSString *text = value;
return text.length > 3;
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

运行程序,然后在文本框继续输入字符,你会发现控制台在textfield的内容长度超过3的时候才打印:

1
2
3
4
5
6
7
8
9
10
11
12
2013-12-26 08:17:51.335 RWReactivePlayground[9654:a0b] is t
2013-12-26 08:17:51.478 RWReactivePlayground[9654:a0b] is th
2013-12-26 08:17:51.526 RWReactivePlayground[9654:a0b] is thi
2013-12-26 08:17:51.548 RWReactivePlayground[9654:a0b] is this
2013-12-26 08:17:51.676 RWReactivePlayground[9654:a0b] is this
2013-12-26 08:17:51.798 RWReactivePlayground[9654:a0b] is this m
2013-12-26 08:17:51.926 RWReactivePlayground[9654:a0b] is this ma
2013-12-26 08:17:51.987 RWReactivePlayground[9654:a0b] is this mag
2013-12-26 08:17:52.141 RWReactivePlayground[9654:a0b] is this magi
2013-12-26 08:17:52.229 RWReactivePlayground[9654:a0b] is this magic
2013-12-26 08:17:52.486 RWReactivePlayground[9654:a0b] is this magic?

其实,在这里你创建了一个非常简单的管道。这就是响应式编程的本质,通过数据流的方式来表达应用程序的功能。下面的图片看起来更为直观:
Alt text

上图可以看到,rac_textSignal是事件的初始来源,数据流通过一个filter过滤,该filter仅允许字符长度大于等于3的事件通过。该管道的最后一步是subscribeNext:,在这一步可以通过block打印事件的值。

这里需要注意的一点是,filter的返回值也是RACSignal,可以通过下面的方式揭示管道的执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
RACSignal *usernameSourceSignal = 
self.usernameTextField.rac_textSignal;

RACSignal *filteredUsername = [usernameSourceSignal
filter:^BOOL(id value) {
NSString *text = value;
return text.length > 3;
}];

[filteredUsername subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

因为在RACSignal执行的每一种操作符返回还是RACSignal,因此也被称为fluentinterface。该功能可以使你构造管道,而无需用局部变量引用每个步骤。
> Note: ReacticeCocoa使用了大量的blocks。如果你之前没有接触过block,你应该先看看Apple的Blocks Programming Topics,如果你像我一样对block很熟悉,但是对语法有点疑惑的话,可以访问http://fuckingblocksyntax.com/来巩固下你的知识。

####隐式转换

将之前的拆分的代码恢复成流式语法:

1
2
3
4
5
6
7
8
[[self.usernameTextField.rac_textSignal
filter:^BOOL(id value) {
NSString *text = value; // implicit cast
return text.length > 3;
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

上面指定位置代码的隐式转换不够优雅,因为传递给该block的值始终是NSString类型,所以可以直接更改参数类型本身,更新代码如下:

1
2
3
4
5
6
7
8
[[self.usernameTextField.rac_textSignal
filter:^BOOL(NSString *text) {
return text.length > 3;
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

运行代码,会发现和之前的效果一致。

什么是 【事件 】?

到目前为止,本教程已经描述了不同的事件类型,但是没有详细介绍这些事件的结构。有趣的是,事件可以包含任何东西!

为了证明这一点,向该管道添加另一种操作符,更新代码如下:

1
2
3
4
5
6
7
8
9
10
11
[[[self.usernameTextField.rac_textSignal
map:^id(NSString *text) {
return @(text.length);
}]
filter:^BOOL(NSNumber *length) {
return [length integerValue] > 3;
}]
subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

运行代码,发现控制台将会打印text field的内容长度而不是内容本身:

1
2
3
4
5
6
7
8
9
10
2013-12-26 12:06:54.566 RWReactivePlayground[10079:a0b] 4
2013-12-26 12:06:54.725 RWReactivePlayground[10079:a0b] 5
2013-12-26 12:06:54.853 RWReactivePlayground[10079:a0b] 6
2013-12-26 12:06:55.061 RWReactivePlayground[10079:a0b] 7
2013-12-26 12:06:55.197 RWReactivePlayground[10079:a0b] 8
2013-12-26 12:06:55.300 RWReactivePlayground[10079:a0b] 9
2013-12-26 12:06:55.462 RWReactivePlayground[10079:a0b] 10
2013-12-26 12:06:55.558 RWReactivePlayground[10079:a0b] 11
2013-12-26 12:06:55.646 RWReactivePlayground[10079:a0b] 12

新增的map操作通过提供的block来转换事件数据流。对于接收到的每一个next事件,它都会执行该block, 然后发出返回值,该返回值仍旧是一个next事件。上面的代码中, map操作符拿到NSString类型的值,获得其长度,并将其转换成NSNumber类型返回。

有关此功能的图形描述,可以看下面的图片:
Alt text

如你所见,map操作符之后的所有的步骤收到的都是NSNumber实例。你可以使用map操作符将收到数据转换成任意类型的对象。

Note: 上面代码中的text.length 返回的是NSInteger类型,它是一种基本类型。为了将其作为事件的内容来使用,必须将其进行包装。Objectice-C提供了一种简洁的字面量语法来进行此此操作@(texr.length)

现在应该使用所学的概念来更新ReactivePlayground的代码。

创建 Valid State Signals

首先要做的就是,创建一对信号来指示usernamepassword是否是有效的。在RWViewController.m的viewDidload方法中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
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]);
}];

如你所见,上面的代码将map操作符应用到每一个textfield的rac_textSignal,输出由Bool值封装的NSNumber对象。

接下来继续转换这些信号,以便它们能够为textfield提供合适的背景色。你通过订阅这些信号,拿到相应的值就可以更新textfield的背景色,一个可行的选择如下:

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

先不要添加上面的代码,因为我们还有更加优雅的方式。

我们的目的是将信号的输出值赋值给textfield的background属性,但是上面的代码并不具备很好的表达性,赋值语句太过于靠后了。

幸运的是ReactiveCocoa有一个宏定义,允许你更加优雅的来表达这个功能。将下面的代码直接添加到viewDidload中两个signals的下方。

1
2
3
4
5
6
7
8
9
10
11
12
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宏定义允许你将信号的输出赋值给对象的属性,它需要两个参数,第一个是包含要设置属性的对象。第二个是参数是属性名称。每次信号发出下一个事件时,传递的值都会赋值给给定的属性。

是不是非常优雅的解决方案?

最后一件要做的事情是移除洗面的代码

1
2
3
self.usernameTextField.backgroundColor = self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor];
self.passwordTextField.backgroundColor = self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];

通过下面的图像,可视化当前的逻辑。可以看到,这里有两个简单的管道,它们获取文本信号,然后通过map将它们映射为有指示有效性的Bool值,然后再映射为UIColor,将UIcolor绑定到textfield 的backgroundcolor上。
Alt text

你是否好奇,为什么创建了两个分离的信号validPasswordSignalvalidUsernameSignal,而不是为每个textfield创建一个单一的流畅管道。保持耐心,这种疯狂背后的方法很快将变得清晰!!!

Combining signals (组合信号)

当前情况下, Sigin InButton只有在username和password输入框都有效的情况下才能点击,是时候通过响应式的方式来做这件事情了。

当前的代码已经有能发出boolean类型值的信号,来显示username和password是否是有效的:validUsernameSignalvalidPasswordSignal。你要做的就是将这两种信号组合起来,以确定何时是该Button处于enable状态。

在viewDidload方法中添加下面的代码:

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

上面的代码通过combineLatest:reduce:方法组合validUsernameSignalvalidPasswordSignal提交的最新值成为一个新的信号。每当两个信号中的任意一个提交了新值reduce block都会执行,其返回值将成为组合信号的下一个值。

RACSignal组合方法可以组合任意数量的信号,reduce block中的参数对应着每个源信号的值。ReacticeCocoa有一个小的实用类RACBlockTrampoline,该类用来处理reduce block中的可变参数列表。

现在你就有了一个合适的信号,继续添加下面的代码到 viewDidload方法的最后:

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

在运行程序之前,移除下面的属性和代码

1
2
3
4
5
6
7
8
9
10
11
12
@property (nonatomic) BOOL passwordIsValid;
@property (nonatomic) BOOL usernameIsValid;

----------------------------
// handle text changes for both text fields
[self.usernameTextField addTarget:self
action:@selector(usernameTextFieldChanged)
forControlEvents:UIControlEventEditingChanged];
[self.passwordTextField addTarget:self
action:@selector(passwordTextFieldChanged)
forControlEvents:UIControlEventEditingChanged];

同时也移除updateUIStateusernameTextFieldChangepasswordTextfieldChange方法。最后确保移除viewDidload中对updateUIState的调用。

运行程序,测试Sign In button,当username和password都是有效的时候,Sign In Button应该也是enabled的。这个逻辑如下图:
Alt text

上图揭示了一对非常重要的概念,这对概念可以让你用ReactiveCocoa执行非常强大的任务

  • 拆分 - 信号可以有多个订阅者,并充当多个后续管道步骤的源。上图中,指示username和password有效性的boolean信号被拆分,并用于不同的目的。
  • 组合 - 可以组合多个信号创建新的信号,上图只是组合的boolean信号,实际上你可以组合任意类型的信号。

这些更改的结果是不再需要指示两个textfield是否有效的私有属性,这是使用响应式的主要特征之一 : 不再需要实例变量来追踪瞬时状态。

Reactice Sign-In (响应式登录)

目前为止,只有管理textfield和button的状态使用到了响应式编程的方式,但是按钮的点击事件处理仍旧在使用action的方式,接下来要做的就是用响应式的方式来做替代action的方式。

Sign In Button的点击事件通过storyboard action的方式写在了RWViewController.msignInButtonTouched中。我们要做的就是取代这种方式,所以第一步就是做的就是断开和storyboard action的关联。

打开Main.storyboard,找到Sign In Button,点击crtl键打开outlet/action连接,点击X移除连接,下图显示在哪里可以找到删除按钮:
Alt text

你已经知道ReactiveCocoa 框架如何给UIKit标准控件添加属性和方法。在此之前,你是用的是rac_textSignal,它在text改变的时候提交事件。为了处理事件,你需要另一种方法: rac_signalForControEvents.

回到RWViewController.m,在viewDidload方法最后添加如下代码:

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

上面的代码从Button的UIControlEventTouchUpInside事件创建了一个的信号,并添加订阅,以便每次该事件发生的时候都会打印日志。

运行程序,当username和password有效的时候,点击该button, 查看控制台输出日志:

1
2
3
4
5
2013-12-28 08:05:10.816 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:11.675 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.605 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.766 RWReactivePlayground[18203:a0b] button clicked
2013-12-28 08:05:12.917 RWReactivePlayground[18203:a0b] button clicked

现在按钮有了点击事件的信号,下一步是将此与登录过程本身关联起来。这带来一些问题,但是没关系,你并不介意这些问题,对吗? 打开RWDummySigInService.h:

1
2
3
4
5
6
7
8
9
10
typedef void (^RWSignInResponse)(BOOL);

@interface RWDummySignInService : NSObject

- (void)signInWithUsername:(NSString *)username
password:(NSString *)password
complete:(RWSignInResponse)completeBlock;

@end

该接口将usernamepasswordcompleteBlock作为参数,当登录成功或者失败的时候执行completeBlock。你可以直接在button的subscribeNext:的block中调用该接口,为什么你能这么做呢?因为这种异步的、基于事件的行为,对ReactiveCocoa来说就是家常便饭。

Note: 为了简单期间,本教程使用的简单的虚拟服务,这样就不依赖任何外部的API。但是现在有一个非常现实的问题,怎么使用未用信号表示的API。

Creating Signals (创建信号)

幸运的是,将现有的异步API调整为信号相当容易。首先移除signInButtonTouched:方法,该方法将被其他逻辑所取代。
RWViewController.m中添加下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
-(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;
}];
}

上面的方法创建了使用当前的username和password登录的信号,现在将其进行拆解:

上面的代码使用RACSignal中的createSignal:方法创建信号,该方法的参数是一个Block,用来描述该信号,且该Block只有一个参数。当信号有订阅者的时候,该Block中的代码将会执行。

传递给该Block的是一个subscriber实例,该实例遵守RACSubscriber协议,该协议中有你用来发出事件的方法,你可以发送任意个数的next事件,这些事件会因为error或者complete事件结束。本教程中,将会发送一个next事件来显示是否登录成功或者失败,然后发送 comlete事件结束。

这个Block的返回值是一个 RACDisposable对象,它允许你执行取消或者取消订阅的时候可能需要的任何清理工作。此信号因为没有任何清理需要,所以直接返回nil;

正如你所看到的,在信号中包装异步的API非常的简单。

现在我们来利用这个信号,将下面的代码添加到viewDidload的最后

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

上面的代码通过map操作符将登录信号转换为登录信号,订阅者只打印结果。

直接运行该代码,然后点击Sign InButton,查看XCode的控制台打印,将会看到下面的结果,该结果也许和你想象的不太一样:

1
2
3
2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result:
<RACDynamicSignal: 0xa068a00> name: +createSignal:

subscribeNext:block传递的是整个信号,而不是登录信号的结果。

来看下这张图:
Alt text

当你点击button的时候,rac_signallForControlEvents发出一个next事件,然后map将创建并返回登录信号,这意味着下面的管道步骤接收的是RACSignal。这就是你在subscribeNext:中观察到的内容。

上面中情况称为信号中的信号,换言之是包含内部信号的外部信号。如果你想的话,你可以在外部信号的 subscribeNext:中订阅内部信号。但是这样做会导致嵌套混乱!幸运的是,这是一个常见的问题,ReactiveCocoa已经针对这种情况做好了准备;

信号中的信号

此问题的解决步骤非常的简单,只需将map函数改成flattenMap:

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

这段代码同样是将按钮的点击信号映射为登录信号,但是flattens可以将事件从内部信号发送到外部信号。

运行代码,看一下Xcode的控制台:

1
2
3
2013-12-28 18:20:08.156 RWReactivePlayground[22993:a0b] Sign in result: 0
2013-12-28 18:25:50.927 RWReactivePlayground[22993:a0b] Sign in result: 1

现在这个管道正在做的就是你想要的了,最后一步就是在subscribeNext中添加相关的逻辑,以在登录成功之后执行所需的导航。

1
2
3
4
5
6
7
8
9
10
11
12
13
[[[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];
}
}];

运行程序:
Alt text

你是否注意到当前的应用程序存在一个小的用户体验的问题? 当登录服务验证提供的凭据时,应禁用登录按钮。这样可以防止用户重复相同的登录。此外,如果出现登录尝试失败,当用户再次尝试登录时,错误消息应隐藏。

但是,如何将此逻辑添加到当前管道?更改按钮的enable状态不是transformationfilter或到目前为止遇到的任何其他概念。其实,它被称为副作用管道中next事件发生时执行的逻辑,它实际上不会更改事件本身。

Adding side-effects(添加副作用)

用下面的代码取代当前的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[[[[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];
}
}];

你可以看到上面如何添加 doNext:,在按钮触摸事件创建后立即向管道添加该步骤。请注意, doNext: 块没有返回值,因为它是副作用,它使事件本身保持不变。

上面的 doNext: 块将按钮的属性设置为 NO,并隐藏失败文本。当订阅Next:块的时候重新启用按钮,并根据登录结果显示或隐藏失败文本。

Alt text

运行应用程序以确认登录按钮按预期启用和禁用

现在,你的工作就完成了 – 应用程序现在完全是Reactive状态。

Note: 当异步事件正在进行的时候禁用button是一个常见的问题,如果ReacticeCocoa中遍布这种处理就会显得非常混乱。RACCommand 封装了这个概念,并具有enabled信号,允许您将Button的enable属性连接到信号, 可能你需要尝试该类。

####结论

希望本教程已经给你一个良好的基础,这将有助于你在自己的应用程序中使用ReactiveCocoa框架。熟悉这些概念可能需要多加练习,但与任何语言或程序一样,一旦你找到它的窍门,它真的很简单。ReactiveCocoa的核心是信号,它们只不过是事件流。还有什么比这更简单的呢?

对于ReactiveCocoa,我发现的有趣的事情之一是有许多方法可以解决同样的问题。你可以通过这个应用程序练习,通过调整信号和管道,以更改它们拆分和组合的方式。

ReactiveCocoa的主要目标是使代码更简洁、更易于理解。就个人而言,我发现,如果应用程序的逻辑表示为清晰的管道,使用流畅的语法,则更容易理解应用程序的作用。

在本教程系列的第二部分中,你将了解更高级的主题,如错误处理以及如何管理在不同线程上执行的代码。

Part-03 原文

ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2