文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

作为一个 iOS 开发者,编写的每一行代码几乎都是在做事件的交互。按钮的点击,从网络上收到一个消息,一个属性被修改(KVO),或者通过CoreLocation接收用户地点的改变,这些都是很好的例子。但是,这些事件都是通过不同的途径表达,例如actions,delegate (委托), KVO, callback (回调)和其他。ReactiveCocoa 给事件定义了一个标准的接口,所以我们可以通过它更容易的实现链接,过滤,组合。

感觉很疑惑?很有趣?思维转不过来?那就继续往下阅读吧:]

ReactiveCocoa 组合了几种编程范式:

第 1 段(可获 2 积分)
  • Functional Programming (函数式编程)通过高阶函数实现编码,例如接收其它函数作为函数的参数。
  • Reactive Programming (响应式编程)通过专注于数据流和数据的改变。

所以,你可能听到别人会已函数响应式编程框架(FRP)来描述 ReactiveCocoa 。

但是,你大可放心,这些都是理论学习教程的东西,本文不会大幅度的讲解理论。编程范式却是是一个很吸引人的主题,但是下文将把重点放在如何把ReactiveCocoa应用于实践中,还有通过一些例子来讲解,而不会大幅度的涉及理论知识。

第 2 段(可获 2 积分)

Reactive Playground

通过本文讲解ReactiveCocoa, 你将会从一个非常简单的例子(ReactivePlayground)中了解到什么是响应式编程。下载 演示项目, 并编译运行,验证你的开发环境是否已一切准备妥当。

ReactivePlayground是一个非常简单的用户登录app。用户输入正确的用户密码后,app将会展示一幅可爱的猫咪图像。

ReactivePlaygroundStarter

哇!好可爱哦!

现在,让我们一起浏览一下演示项目的源代码。这个项目的代码非常简单,所以你不用花费很多时间就可以阅读完毕。

第 3 段(可获 2 积分)

打开RWViewController.m 并快速浏览一下。你能很快的找到什么情况下,登录按钮会变为可用?还有signInFailure label会在什么情况下显示或隐藏? 如果换作是一个类似的简单的项目,你肯定在1-2分钟内找到答案。在一个相对复杂的项目,可能也会再多花一点点时间。

而使用了ReactiveCocoa之后,应用的意图会立刻变得非常直接明了。好了,让我们来开始吧!

添加 ReactiveCocoa Framework

第 4 段(可获 2 积分)

添加ReactiveCocoa framework到你的项目中最简便的方法就是通过 CocoaPods. 如果你从来没有使用过 CocoaPods,你可以阅读 CocoaPods 简介 教程, 或者从最基本的运行初始化步骤,或者你可以从头运行一次那个教程,以便做好的前置的环境配置。

注意事项: 如果出于某些原因,你不能使用CocoaPods, 你仍然可以使用ReactiveCocoa, 只需根据 Github文档的步骤 Importing ReactiveCocoa 配置即可。

如果你的xcode已经有ReactivePlayground项目, 那么请你先把xcode关闭。CocoaPods 将创建一个 Xcode 工具区(workspace), 这个工作区将用于代替原来的项目文件。

第 5 段(可获 2 积分)

下一步打开终端,路径切换至你的项目文件所在的文件夹,并输入如下命令:

touch Podfile
open -e Podfile

这一步将会创建一个空的,名为 Podfile 的文件,并使用 TextEdit 打开该文件。复制粘贴如下内容到TextEdit 窗口:

platform :ios, '7.0'
 
pod 'ReactiveCocoa', '2.1.8'

这一步,将设置开发平台为 iOS, 最小的 SDK 版本设置为 7.0, 并添加 ReactiveCocoa 框架作为本项目的一个依赖包。

保存并返回到终端窗口,并输入如下命令:

pod install

你将会看到类似如下的信息:

Analyzing dependencies
Downloading dependencies
Installing ReactiveCocoa (2.1.8)
Generating Pods project
Integrating client project
 
[!] From now on use `RWReactivePlayground.xcworkspace`.

提示信息告诉你 ReactiveCocoa 已下载完毕,并且CocoaPods将帮你创建一个新的Xcode 工作区(workspace),把该框架集成到你的应用当中。

第 6 段(可获 2 积分)

我们可以打开新生成的工作区, RWReactivePlayground.xcworkspace, 并通过项目导航器查看CocoaPods为我们创建的项目结构:

AddedCocoaPods

你会发现, CocoaPods 创建了一个新的工作区( workspace)并添加原来的项目RWReactivePlayground到工作区,同时声称了一个名为 Pods 的项目,该项目包含了 ReactiveCocoa。 CocoaPods 可以如此轻松的帮我们管理包依赖,太棒了!

你会发现,项目的名字叫做 ReactivePlayground, 就如这个项目的名字,我们可以开始和reactivecocoa一起玩耍…

开始和ReactiveCocoa玩耍

就如前面介绍提到的, ReactiveCocoa提供处理app里面的事件流的标准的接口。用 ReactiveCocoa的语言,我们管这个叫做信号( signals),并通过 RACSignal类实现

第 7 段(可获 2 积分)

打开app的初始化view controller文件, RWViewController.m, 并通过添加如下行引入ReactiveCocoa的头文件:

#import <ReactiveCocoa/ReactiveCocoa.h>

目前为止,你还没有替换原来的任何代码,你只是开始接触reactivecocoa库。添加如下行到 viewDidLoad 方法的末尾:

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

编译并运行,然后在输入框中随便输入些字符。留心观察输出终端 console, 你会看到类似如下的信息:

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?

每次当你改变输入框的字符的时候,你都会看到终端输出一行类似的信息。没有 target-action, 没有 代理 delegates, 只有信号signals 和 blocks. 是不是很令人振奋?

第 8 段(可获 2 积分)

ReactiveCocoa 信号 (通过 RACSignal实现) 发送一个事件流给它的订阅者( subscribers). 你需要知道,有如下3种事件: next, error 和 completed. 一个信号 signal 可以在发送complet事件前,发送任意个 next 事件 。在这一部分的教程中,我们会把注意力集中在next 事件上。当你需要学习error 和 completed事件的时候,请认真阅读第二部分。

RACSignal 提供多个方法,让你订阅不同的事件。每个方法都接受一个或多个 blocks, 在block里面,当有事件发生的时候,运行你需要的业务逻辑。在上面的例子中,你会看到我们给 subscribeNext: 指定一个block,并在每个 next 事件发生的时候,调用这个block。

第 9 段(可获 2 积分)

ReactiveCocoa框架使用类别categories向标准的UIKit类添加对信号signal的支持,所以你可以向这些控件发生的事件添加订阅者。就如这里,输入框用到的rac_signal属性。

理论知识已经阐述完毕,让我们开始使用 ReactiveCocoa 来创建一些好玩的应用吧!

ReactiveCocoa 提供丰富的函数和方法,以便我们更好的操作事件流。例如,加入你只对用户名输入框中,文字长度大于某个值的时候,才感兴趣,那么你可以使用filter方法。按如下所示,修改你刚才的添加到 viewDidLoad 的代码:

第 10 段(可获 2 积分)
[[self.usernameTextField.rac_textSignal
  filter:^BOOL(id value) {
    NSString *text = value;
    return text.length > 3;
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  }];

如果你编译运行,并在输入框输入一些文字,你会发现控制台只有在输入框的文字长度大于3的时候才打印信息:

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?

在这里,我们使用了非常简单的事件管道 pipeline。这是Reactive编程的核心,通过这个模式,你可以以数据流的形式来表达你的业务逻辑。

下图可以更好的帮我们理解事件流:

FilterPipeline

在上面的图中,你会看到rac_textSignal是事件的源头。数据通过 filter 后,只会允许满足条件:文字长度大于3的数据通过。管道的最后一步是 subscribeNext: 事件,这里是完成事件包含的值的输出。

第 11 段(可获 2 积分)

到这里为止, filter 方法的输出仍然是一个 RACSignal。你可以按如下方式管理你的代码,把各个步骤分解出来:

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,这叫做 fluent interface。这个特性允许你构建多个管道,而不需要为管道中的每一个步骤创建一个本地变量。

注意:ReactiveCocoa 非常依赖代码块blocks。如果你对代码块 blocks 不是很熟悉,你需要先阅读苹果 Blocks Programming Topics。如果你像我一样,对 blocks 非常熟悉,但是仍然对这个语法感到疑惑,你可以阅读一下这篇文章: f*****gblocksyntax.com ,它非常有用。 (这篇文章的网址包含粗俗内容,所以用星号代替,但是网址链接是有效的。)

第 12 段(可获 2 积分)

一点小转换

现在你可以从切分的事件流中,合并为最开始的链式语法:

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

在这里,隐式的从 id 转变为 NSString,虽然上面的代码不是很优雅。但幸好,由于调用blocks的参数永远都为 NSString类型,所以你可以把参数类型修改为NSString:

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

编译并运行,并确保输出结果跟刚才一样。

什么是事件Event?

目前为止,我们阐述了几种事件类型,但还没有深入描述这些事件的结构。有趣的是,一个事件可以包含任何类型的数据!

第 13 段(可获 2 积分)

为了阐述这点,我们向管道添加另外一个操作。按照如下代码,更新刚才添加至 viewDidLoad 的代码:

[[[self.usernameTextField.rac_textSignal
  map:^id(NSString *text) {
    return @(text.length);
  }]
  filter:^BOOL(NSNumber *length) {
    return [length integerValue] > 3;
  }]
  subscribeNext:^(id x) {
    NSLog(@"%@", x);
  }];

如果你编译并运行,你会发现,控制台输出的是字符串的长度,而不是字符串本身:

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 的类型返回。

以下的图,将更直观的展示事件流的过程:

第 14 段(可获 2 积分)

FilterAndMapPipeline

 你会看到,管道中map操作后面所有的步骤都会接收到一个NSNumber 实例。你可以使用 map 操作转换为任何一种你需要的数据,只要它是对象object即可。

注意:上面的例子中 text.length 属性返回的是一个 NSUInteger 标量类型。为了可以传递给下一个事件,你需要把它装箱为对象。幸好, Objective-C literal syntax 这篇文章提供一个非常简单的装箱方法给我们:@(text.length).

好了,是时候用我们所学的理论来更新我们的ReactivePlayground app代码。你需要把刚才的演示代码给删除。

第 15 段(可获 2 积分)

创建数据验证信号

第一件事,我们需要创建一些信号,来验证用户输入的用户名和密码是否正确。向  RWViewController.m 的 viewDidLoad 方法的末尾添加如下代码 :

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]);
    }];

就如你看到的, 我们为每一个 rac_textSignal的信号都添加一个map 方法。map方法的输出是一个 以NSNumber 封装的 boolean 类型的数据。

下一步,就是把这些信号转变为更醒目的背景颜色,以便用于输入框中。我们可以订阅这个事件,并把这个颜色应用于输入框中。其中一个可行的方法如下所示:

第 16 段(可获 2 积分)
[[validPasswordSignal
  map:^id(NSNumber *passwordValid) {
    return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    self.passwordTextField.backgroundColor = color;
  }];

(请不要使用上面的代码,更优雅的实现方法在后头呢!)

理论上,你是向输入框的信号中添加backgroundColor属性。但是,上面的代码并不是最佳实践。

幸好,ReactiveCocoa给我们提供一个宏,允许我们更直观,更优雅地表达我们的想法。把下面的代码直接添加到viewDidLoad新增的那两个信号后面:

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 宏允许我们把信号的输出赋值给对象的属性。它接受两个参数,第一个参数是包含被设置属性的对象,第二个是需要设置的属性。每当信触发一个next事件的时候,信号的值就会赋值给对应的对象属性。

第 17 段(可获 2 积分)

这是个非常优雅的实现方式,不是吗?

最后,只需编译运行。找到updateUIState方法,并删除方法前面两行:

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

这一步将删除那些非函数式变成的代码。

编译并运行程序。你会发现,输入框会在输入不正确的时候,高亮显示,并在输入合法的情况下,清除高亮。

看起来不错,那么下图将用图形化的方式来描述当前的业务逻辑。你会看到,两个管道接收输入框的信号,并把输出映射为输入是否合法的布尔值,接着再转换为绑定至输入框背景颜色的UIColor实例。

第 18 段(可获 2 积分)

TextFieldValidPipeline

你可能会问,为什么我们要分别创建验证密码是否正确的validPasswordSignal信号和验证用户名是否正确的 validUsernameSignal信号,而不去把那两个输入框输出的信号合并为一个管道? 读者情耐心点,这个做法的理由后面会越来越清晰!

合并信号

在本应用中,登陆按钮只有在用户名和密码输入框的内容都合法的情况下才能允许点击。该是时候使用reactive的风格实现啦!

当前的代码已经为我们生成了一个布尔值,以表示用户名和密码输入框的内容是否合法,它们分别是validUsernameSignal和 validPasswordSignal。接下来的任务,就是把这两个信号合并为一个,并判断登陆按钮是否应该设置为可用。

第 19 段(可获 2 积分)

viewDidLoad的末尾,添加如下代码

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

上面的代码合并使用combineLatest:reduce: 方法合并信号validUsernameSignal 和 validPasswordSignal输出的最新的值,并生成一个小的信号。每当其中一个信号输出一个新的值,reduce代码块都会执行一次,并且该方法返回的值都会以合并的next值的形式发送出去。

注意:RACSignal combine 方法可以合并任意多的信号,reduce代码块的参数必须与每一个合并信号相对应。ReactiveCocoa 提供给一个方便的工具类,名为 RACBlockTrampoline ,它是ReactiveCocoa内部用于处理reduct代码块的参数。实际上,ReactiveCocoa内部隐藏了好多有意思的代码实现方法,所以值得我们深入去阅读它底层的实现方法!

第 20 段(可获 2 积分)

现在,你得到了一个合适的信号,在viewDidLoad方法的末尾添加如下代码。它将帮我们实现信号与按钮可用与否的绑定工作:

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

在运行该代码前,是时候把旧的实现方法代码给删除。在类的属性定义中,移除以下两行:

@property (nonatomic) BOOL passwordIsValid;
@property (nonatomic) BOOL usernameIsValid;

在 viewDidLoad 方法靠近最前面的地方,移除以下行:

// 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];

并在 updateUIState中移除 usernameTextFieldChanged 和 passwordTextFieldChanged 这两个方法。哇!你刚才移除了好多非函数式编程的代码! 你会感恩你刚才所做的。

最后,确保你同时移除了viewDidLoad方法中,调用updateUIState方法的代码。

第 21 段(可获 2 积分)

如果,你编译并与运行,你会发现,登陆按钮是在可用状态,因为用户名和密码输入框的输入同时有效。就像之前的情况一样。

让我们来更新流程图,反应最新的应用的业务逻辑:

CombinePipeline

上图揭示了几个重要的理论,它们允许你通过ReactiveCocoa执行一些强大的任务:

  • 切分 – 信号可以有多个订阅者,并可以作为多个管道步骤的源头。在上图中,我们注意到,标识用户名和密码输入是否正确的布尔值信号被切分,并用于多个为了实现不同目的步骤中。
  • 合并 – 多个信号可以合并为一个新的信号。在本例子中,两个布尔值信号被合并。而且,你可以合并各种生成任意值的信号。
第 22 段(可获 2 积分)

通过这些更改,我们的应用就不再包含标识用户名和密码是否正确的私有变量。这是我们采用响应式编程的其中一个关键不同点。你不再需要使用实例变量去监测实例的瞬间状态。

响应式登录

这个应用现在使用上面描述的响应式管道来管理输入框的状态和按钮的状态。但是,按钮的点击的响应仍然使用actions模式,所以下一步就是替代剩下的逻辑,让它更符合响应式编程。

第 23 段(可获 2 积分)

登录按钮的Touch Up Inside事件通过storyboard故事板绑定了RWViewController.m文件中的signInButtonTouched 方法。我们将使用响应式编程的方法替代这些代码。所以,第一步,我们需要在故事板storyboard中解除这个事件绑定。

打开 Main.storyboard, 找到登录按钮, 按着ctrl并点击它,此时会出现 outlet / action 链接菜单,现在点击 x 按钮,删除这个链接。如果你感觉很迷惑,下图将帮助你如何找到删除按钮:

DisconnectAction

你现在看到 ReactiveCocoa 框架向基本的UIKit空间增加了额外的属性和方法。目前为止,我们使用了 rac_textSignal,它可以在输入框内容改变的时候,发送事件。为了处理这些事件,我们需要另外一个ReactiveCocoa adds 向 UIKit添加的方法: rac_signalForControlEvents。

第 24 段(可获 2 积分)

回到 RWViewController.m, 并添加如下代码到 viewDidLoad: 方法的末尾:

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

上面的代码在登录按钮的UIControlEventTouchUpInside事件中创建一个信号,并添加一个订阅者,用于在每次该事件触发后都记录一次信息。

编译并运行,并确保信息确实写入到控制台中。请牢记,按钮只有在用户名和密码输入都正确的时候才会变为可用状态,所以在点击按钮前,请确保两个输入框都已经输入了字符!

你会在xcode的控制台中看到类似下面的消息:

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

现在,按钮的点击事件包含了一个信号,下一步就是绑定登录的逻辑到这个信号中去。这里可能会有些问题,但是无妨,你不会介意的,对吧?打开  RWDummySignInService.h 文件,并看看这个接口文件:

第 25 段(可获 2 积分)
typedef void (^RWSignInResponse)(BOOL);
 
@interface RWDummySignInService : NSObject
 
- (void)signInWithUsername:(NSString *)username
                  password:(NSString *)password 
                  complete:(RWSignInResponse)completeBlock;
 
@end

这个服务函数接受 一个username 用户名参数, 一个 password 密码参数, 还有一个回调 completion 代码块参数。这个代码块会在登录成功或失败后被调用。你可以直接在当前只是输出信息的subscribeNext:代码块中使用这个接口。这是一个ReactiveCocoa常用的异步的,基于事件的方法。

注意: 在本教程中,我们使用伪代码服务逻辑来实现登录,所以你不需要任何外部的 APIs 作为依赖。但是,你现在遇到一个非常现实的问题,我们该如何以信号的形式来调用 APIs ?

第 26 段(可获 2 积分)

创建信号

幸好,基于外部APIs来创建异步信号是非常简单的事情。第一步,删除 RWViewController.m 中的signInButtonTouched: 方法。我们不再需要这个逻辑,而是用响应式编程的方法来实现。

在 RWViewController.m 文件中添加如下方法:

-(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: 方法来创建信号。该代码块用一个参数来代替信号,并赋值给这个方法。当这个信号有一个订阅者的时候,该代码块里面的代码就会被调用执行。

第 27 段(可获 2 积分)

代码块接受一个遵循RACSubscriber协议的一个订阅者对象subscriber作为参数,该协议包含发送事件的方法。你可以发送任意多个next事件,并以complete或者error事件作为结束。在本例子中,我们发送一个next事件,表示当前的登陆操作是否成功,并以一个complete事件作为结束。

代码块返回的是一个 RACDisposable对象,它允许我们在订阅者被取消后,或者销毁后,做的一些后续清理的操作。这个信号并没有任何清理操作,所以我们用nil作为返回值。

第 28 段(可获 2 积分)

就如你看到的,通过信号包装一个异步API的方法的编程方式是那么的精简!

现在,是时候使用这个新创建的信号。在 viewDidLoad的末尾增加如下代码:

[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   map:^id(id x) {
     return [self signInSignal];
   }]
   subscribeNext:^(id x) {
     NSLog(@"Sign in result: %@", x);
   }];

上面的代码使用了前面用到的 map 方法,把按钮点击事件转换为登录信号。订阅者只是简单的记录登录的结果。

编译并运行,然后点击登陆按钮,现在留意一下 Xcode 的控制台,你就可以看到上面代码的输出结果 …

… 但是这个输出结果跟你预期的貌似不一样!

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

subscribeNext:代码块确实被传递了一个的信号,可是这个信号不是登录信号的结果!

第 29 段(可获 2 积分)

现在是时候用图表来描述信号管道关系,好让读者知道我们到底做了什么:

SignalOfSignals

当你点击按钮的时候,rac_signalForControl 事件发送一个next事件 (并使用 UIButton 作为它的事件数据) 。map 映射步骤创建并返回一个登录信号,这意味着之后的管道会接收到一个 RACSignal信号。这个信号就是我们在 subscribeNext:方法中观察的信号。

上面的场景,我们通常把它叫做信号的信号(signal of signals); 换而言之,一个信号内部包含另外一个信号。如果你确实需要,你可以订阅通过外层的信号的subscribeNext:代码块订阅内部信号的事件。但是,这会导致代码块的嵌套而引起混乱。幸好,这个常见的问题,ReactiveCocoa已经有方法应对。

第 30 段(可获 2 积分)

信号的信号

这个问题的解决办法很直观,只需按如下代码把 map 方法改为 flattenMap 方法:

[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   flattenMap:^id(id x) {
     return [self signInSignal];
   }]
   subscribeNext:^(id x) {
     NSLog(@"Sign in result: %@", x);
   }];

这个方法把跟之前一样,把按钮的点击事件转换为登录信号,但不同的是,这个方法同时把信号扁平化( flattens ),并把内部的信号的事件发送给外部信号。

编译并运行,并留意控制台的输出。现在应该可以看到输出的是登录是否成功的信息:

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步骤添加逻辑,以实现登录成功后的切换操作。用以下的代码替代管道中的代码:

第 31 段(可获 2 积分)
[[[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:代码块接受登录信号的输出结果,并对应着更新 signInFailureText 输入框的外观,并按需执行切换操作。

编译并运行,并享受一下响应式编程代码给我们带来的喜悦吧!

ReactivePlaygroundStarter

你是否发现,我们的应用中有一个小小的影响用户使用的问题?当登录服务开始验证登录用户密码的时候,我们需要把登录按钮变为不可用,这样可以防止用户重复点击登录按钮。进一步的说,如果登录失败后,用户在重复登录操作的时候,错误消息必须给隐藏。

第 32 段(可获 2 积分)

但是我们该如何向现有的信号管道添加这段代码? 更改按钮的状态不属于转换操作,也不属于过滤或者其他我们目前为止涉及到的其他事件类型。它应该是一种称为边际效应 (side-effect)的操作; 或者是一段需要在管道发送next事件的同时被执行的逻辑,但它实际上并未改变信号本身。

添加边际效应( side-effects )

使用如下代码替代当前的信号管道:

[[[[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: 代码块并不会返回任何值,因为它只是一段边际效应代码,它并不改变信号本身。

第 33 段(可获 2 积分)

上面的doNext: 代码块把按钮的enabled属性设为NO,并隐藏了失败信息。虽然 subscribeNext: 代码块会重新把登录按钮设为可用,并根据登录结果显示或隐藏错误信息。

是时候更新信号管道的图,包含副作用步骤:

SideEffects

编译并运行,确保登录按钮的可用状态是跟我们的预期一样的。

如果没问题的话,我们的工作就大功告成。现在的应用是完全响应式风格的了!

如果你在某些地方感觉到迷惑,你可以下载项目的最终版本final project (附完整的依赖包),或者你可以从 GitHub下载最新的代码, 每一个提交 commit 都会对应教程中的每个步骤。

第 34 段(可获 2 积分)

注意: 在异步操作过程中禁用按钮是一个常见的问题,但是ReactiveCocoa 却有一些解决方案。RACCommand封装了这个概念,并且有一个 enabled 属性用于绑定按钮的可用状态,你也许可以研究一下这个类。

结论

幸好,这篇文章给你一个很基础的概念,帮助你去开始使用 ReactiveCocoa开发应用。你需要实际操作去体会这些概念,但是就像其他语言或程序,当你掌握了它之后,你会发现它是那么的容易。 ReactiveCocoa的核心概念是信号, 而信号无非就是事件流。还有什么比它更简单?

第 35 段(可获 2 积分)

在ReactiveCocoa中我发现了一个有趣的东西,那就是我们可以通过很多方法解决一个问题。你可能需要通过这个应用来做一下试验,并调整各个信号,通过切分和合并来改变信号。

我们需要好好理解, ReactiveCocoa的最终目标是为了让我们的代码更加简洁和容易理解。就我个人理解而言,我发现以事件管道的方式去组织代码,会让逻辑变的更加容易明白。

在教程的第二部分,你将会学习到更多的知识,例如错误处理,还有如何在不同的线程中管理你的代码。好了,现在你可以好好开始你自己的实验了!

第 36 段(可获 2 积分)

文章评论