iOS 学习

ReactiveCocoa Tutorial – The Def

2018-04-08  本文已影响7人  毛毛可

原文地址:
https://www.raywenderlich.com/62699/reactivecocoa-tutorial-pt1
如原作者发现有侵权行为可责令我在24小时之内删除,前提是你能看到。
翻译者:毛毛可

身为一个iOS开发者,你写的每一行代码基本上都是在处理某些事件的触发;一次按钮点击,一次网络消息的接收,一个属性的改变(KVO)或用户的定位发生改变.但是,这些事件有都是使用不同的方式实现;比如target-action, delegate, KVO, 回调(block)等等.ReactiveCocoa定义了事件的标准接口,所以就能更简单使用一套基本的工具来链式,过滤和组合.
听上去很难理解?好奇? ... 思维风暴? 那么开始阅读吧 :]

ReactiveCocoa结合了几种编程风格:

因为这样的原因,你可能听说过ReactiveCocoa是函数响应式编程(FPR)框架.
放心吧,这与本教程将要学习的一样!编程范例是一个引人入胜的主题但是本次ReactiveCocoa指南接下来仅仅在实际操作上,通过实现替代原理解说.

The Reactive Playground

在本次ReactivceCocoa指南中,将会通过一个非常简单的应用-ReactivePlayground来介绍响应式编程.下载初始项目,然后运行app来验证所有的设置都正确.

ReactivePlayground是一个很简单的应用,仅仅展示了用户登录页面.给页面提供正确凭证,凭证的内容很有想象力,用户名为user,密码是password,你会看到一只很有爱的小猫咪照片.

15225121541975.jpg

很可爱,是不是!

现在花一点时间看看项目的代码.它非常的简单,不需要花很多的时间阅读.

打开并查看RWViewController.m文件,你最快需要多长时间确认Sign In button enable状态下的条件?signInFailure label的显示/隐藏规则是什么?对于这个相对简单的项目,可能只需要一到两分钟就可以回答出以上问题.对于更复杂的项目,你应该也可以采用相同的分析方式,可能需要很长的时间.

使用ReactiveCocoa后,理解app的这些意图会变得更加清楚.是时候开始了!.

Adding the ReactiveCocoa Framework

给你的项目添加ReactiveCocoa框架最简单的方式就是通过CocoaPods.如果在之前你从来没有使用过CocoaPods,你可能需要通篇浏览Introduction To CocoaPods这个教程,或者至少看看这个教程中介绍的安装步骤章节,这样提前了解如何安装它.

提示:如果你不想使用CocoaPods,你同样也可以使用ReactiveCocoa框架,只需要找到GitHub对应的ReactiveCocoa仓库然后导入即可.

如果ReactiveCocoa项目仍然是开着的,那么现在就关闭它.CocoaPods会创建一个Xcode工作控件,你会使用它来代替原先的项目文件.

打开命令行.进入到此项目文件的具体目录位置,输入下面命令:

touch Podfile
open -e Podfile

上面的命令将会创建一个名为Podfile的空文件,并使用文本编辑器打开此文件.复制粘贴下面的文本到文本编辑器中:

platform :ios, '7.0'

pod 'ReactiveCocoa', '2.1.8'

(译者根据最新的版本更改的)

platform :ios, '9.0'
use_frameworks!
target 'RWReactivePlayground' do
  pod 'ReactiveObjC', '3.1.0'
  target 'RWReactivePlaygroundTests' do
    inherit! :search_paths
  end
end

设置了平台为iOS,最小SDK版本为9.0,添加了ReactiveCocoa(ReactiveObjc)框架作为依赖.
保存了这份文件,回到命令行窗口,执行下面命令:

pod install

你将会看到类似下面的输出:

Analyzing dependencies
Downloading dependencies
Installing ReactiveObjc (3.1.0)
Generating Pods project
Integrating client project

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

这标志着ReactiveCocoa框架已经下载完成,并且CocoaPods创建了一个整合了此框架的Xocde工作空间到你的已存在的应用中.

打开最新的生成的工作空间,RWReactivePlayground.xcworkspace文件,查看一下在Project Navigator内部CocoaPods创建的结构目录:

15225556486695.png 15225558268793.jpg

(最新的)
你应该会看到CocoaPods创建了一个新的工作空间,并添加了原始的项目,RWReactivePlayground,将他们一起和ReactiveCocoa合并到一个Pods项目中.CocoaPods确实能够轻松管理依赖关系!

你一定注意到了这个项目的名称叫ReactivePlayground,也就是意味着是时候正式开始了...

Time To Play

正如介绍之所提到的,ReactiveCocoa提供了标准的接口,用来处理
应用中不同的事件流.用ReactiveCocoa专业术语讲,叫信号(统一称为signal),通过RACSignal类来表示.

打开本app的初始view controller,RWViewController.m,并在文件的最开始导入ReactiveCocoa头文件:

#import <ReactiveCocoa/ReactiveCocoa.h>

你现在不需要修改任何已存在的代码,你只需要稍微的把玩一下.添加以下代码到viewDidLoad方法的最后面:

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

运行app,在username text field中输入一些文字.紧盯着打印台,你会看到类似以下的输出:

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?

你可以看到你每一次在text field中输入文字的改变,都会执行一次block.没有使用target-action, 没有delegates - 仅仅是signal和block.这多么令人兴奋!

ReactiveCocoa信号(用RACSignal表示)给它们的订阅者发送事件流.这里需要知道有三种类型的事件:next, error, completed.一个signal可能在因发生错误而终止之前或事件完成之前发送任意数量的next事件.这一章,你会把焦点聚集在next事件上.一定要阅读第二章,在那里会学errorcompleted事件.

RACSignal提供了很多方法,可以用来订阅不同类型的事件.每个方法都会有至少一个block,当一个事件发生时,就会执行block中的自定义逻辑.比如,你可以看到subscribeNext:方法就提供了一个block来执行每一次next事件.

ReactiveCocoa框架使用分类的方式为标准的UIKit控件添加signal,这样你就可以给它们(控件)添加订阅了,这就是rac_textSignal属性的由来.

理论说这么多了,是时候开始使用ReactiveCocoa来做些事情了!

ReactiveCocoa拥有丰富的操作符用来操作事件流.比如,假如你想让username的长度多余三个字符时才处理事件.你可以使用filter操作符来实现.更新之前在viewDidLoad方法中的代码如下:

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

此时如果你运行app,在username text field中输入文字, 你会发现只有text field文本长度大于三个字符的时候才开始打印:

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?

你创建了一个非常简单的管道.它就是响应式编程的本质,在数据流中表达应用的功能.
通过下面的流程图可以帮助你理解:


15226737055580.png

在上面的图中,你可以看到rac_textSignal是事件来源的开始.数据流使用filter,在字符串的长度大于3的时候,才允许事件通过.管道的最后一步是subscribeNext:方法,在block中打印事件的值.在这一点上值得注意的是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,这被称为链式编程.这样的特性允许你构建管道而不需要使用本地变量来引用每个步骤.

提示:ReactoveCocoa中使用了大量的block,如果你对block的用法还不熟悉,你可能需要阅读Apple的Blocks Programming Topics.假如,你已经非常熟悉block,但是对于语法还是有一些小困惑,你需要阅读 f*****gblocksyntax.com,非常有用.(我们有意的隐藏了部分字符,但是该链接真实有效.)

A Little Cast

如果你刚才讲代码更改成分离式,那么现在请改回成链式语法:

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

在上面的代码中标记的位置(implicit cast)的idNSString的隐式转换不够简洁.幸好,此block的value值都是一个NSString类型,所以你可以更改他的参数类型.更新为如下代码:

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

运行app,像之前那样确认是否能正常工作.

What’s An Event?

目前来说,本指南已经描述了几种不同的事件类型,但是没有详细的描述这些事件的结构.有趣的是,一个事件中可以包含任何东西!
为了说明这一点,你将会添加另一个管道来操作.将下面的更新后代码添加到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);
  }];

如果你此时运行app,你会发现app现在会打输入文本的长度:

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类型.
看一下下面的图片,它描述了是如何工作的:

15228460514353.png

正如你看到的,map操作符后面的步骤都是接收NSNumber实例.你可以使用map操作符将接收到的数据转换成任意类型,只要它是一个对象类型就行.

提醒:上面的例子中,text.length属性会返回一个NSUInteger的基本类型值,为了能让它作为事件中内容,必须要包装.幸运的是Objective-C语法中提供了一种简单方式 - @(text.length).

好了!是时候将刚才所学到的概念运用到ReactivePlayground app上了.你可能要删除之前所有添加过的代码.

Creating Valid State Signals

首先,你需要创建两个signal,来标识usernamepassword 这两个 text field 是否有效.添加下面的代码到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]);
    }];

上面的代码,每个text field的rac_textSignal都使用了map操作,返回的是一个被包装成NSNumber类型的布尔值.

下一步就是使用转换后signal给text field 提供一个合适的background color.基本上,就是你订阅这个signal,并且使用signal的返回结果来更新text field的背景色. 下面的一个可行方式是:

[[validPasswordSignal
  map:^id(NSNumber *passwordValid) {
    return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    self.passwordTextField.backgroundColor = color;
  }];

(不要添加此行代码, 之后会有更优雅的解决方式)

理论上,你通过signal的输出来赋值text field的backgroundColor属性.但是上面的代码太繁琐,全都是回车!
幸好,ReactiveCocoa通过宏来允许你优雅的使用表达式.在viewDidLoad中直接添加下面代码到signal代码的下面:

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宏允许将signal的输出赋值给一个对象的属性.它接收两个参数,第一个参数是包含对应属性的对象,第二个参数是属性名称.
signal每发送一个next事件,输出的值就赋值一次对应的属性.
你不觉得这是一个非常优雅的解决方式吗?
在运行app之前还有一件事.定位到updateUIState方法并删除前两行:

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

删掉非响应式代码.

运行app.你应该会发现当textfield 无效时是高亮状态,有效时则正常.
视图化展示是最好的,所以下面展示了当前逻辑的视图.这里你会看到两个简单的管道,使用text signal,通过map得到是否有效的布尔值,第二次map得到一个UIColor,然后绑定给text field的background color.

15228517528188.png

你是不是想知道为什么要分开创建validPasswordSignalvalidUsernameSignal,而不是使用一个链式管道连接每一个text field?请耐心,此方法会在之后被改造的非常简洁!

Combining signals

在当前的app中,登录按钮只有在usernamepassword text field 输入都有效时才能工作.现在让它变成响应式风格!现在的代码已经有两个signal,发送一个布尔值来标识username 和 password text field 是否有效;分别是validUsernameSignalvalidPasswordSignal.你的任务就是合并这两个signal,再决定何时让登录按钮可用.
添加下面的代码到viewDidLoad方法的最后:

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

上面的代码使用combineLatest:reduce:方法,将validUsernameSignalvalidPasswordSignal发送的最新值合并到一个新的signal中.这两个signal中有任意一方发送了新值,reduceblock就会执行,并且它返回的值作为合并后的signal的下一个值发送.

提示:RACSignalcombine方法可以合并任意数量的signal,不过reduceblock的参数要与每个源signal都符合.ReactiveCocoa有一个工具类,RACBlockTrampoline,他会在在内部处理reduceblock的参数列表.实际上,ReactvieCocoa有大量的小技巧隐藏在内部实现中,很值得去你去研究源码!

现在你已经有了一个合适的signal,将以下代码添加到viewDidLoad最后面.它会连接到button的enabled属性:

[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,usernameTextFieldChangedpasswordTextFieldChanged方法.这些非响应式的代码都处理掉!

最后,确保移除了viewDidLoad方法中的updateUIState调用.

现在运行app,观察一下登录按钮.你应该会看到按钮可用,因为username 和 password text field 都有效.

更新过后的app 逻辑视图:


15229027986537.png

上面的图阐明了两个重要的概念,允许你通过ReactiveCocoa执行一些强大简洁的任务;

通过这些改变后结果就是app不会在使用私有属性来标记两个text field当前的有效性.这也是你使用响应式编程时的不同之处 - 不需要使用变量去跟踪临时的状态.

Reactive Sign-in

当前的app 使用响应式来管理text filed 和按钮的状态.但是,按钮仍然使用target-action模式处理点击,所以为了全面的响应式编程,下一步需要替换剩下的逻辑!

登录按钮的Touch Up Inside事件通过storyboard连接到了RWViewController.m文件中的signInButtonTouched方法.你需要使用相等的响应式来替换它,首先你需要取消当前storyboard的action连接.

打开Main.storyboard, 找到登录按钮(Sign In button),找到outlet/action控制板,点击叉号移除之间的连接.如果你感觉疑惑,下面的图片会告诉你如何找删除按钮:

15229227176278.jpg

你已经知道了如何使用ReactiveCocoa框架来添加属性和方法到标准UIKit控件上.刚才你已经使用了rac_textSignal,当文本发生改变时,就会发送事件.为了处理事件,你需要使用ReactiveCocoa为UIkit提供的另一个方法,rac_signalForControlEvents.

回到RWViewController.m,在ViewDidLoad方法最后面添加下面的代码:

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

上面的代码给按钮创建了一个UIControlEventTouchUpInside事件的signal,并添加订阅操作符,在每次事件发生时打印信息.

运行app确认事件会实际打印.记住,按钮只有在username 和 password有效时,才可用.所以一定要确保两个text field都有输入文本再点击按钮!
你应该会看到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

现在按钮拥有了一个touch事件的signal,下一步就是将它和登录逻辑联系在一起.这期间还有一些问题.不过还好,你不需要在意这些小问题.打开RWDummySignInService.h文件,看一些对外接口:

typedef void (^RWSignInResponse)(BOOL);

@interface RWDummySignInService : NSObject

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

@end

该service对象接收username, password, completion black参数.block会在登录成功或者失败时发生回调.你可以在打印了当前touch事件的subscribeNext:block中直接使用这个接口,但我们为什么要这么做?这是因为ReactiveCocoa是一种基于事件的异步的行为.

提示:本指南为了简单起见,使用了一个虚拟逻辑的service对象,所以你不需要依赖任何额外的API.但是现在有一个问题,你怎样使用没有发送signal(不返回signal)的API?

Creating Signals

幸运的是,将现有的异步API转换成singal是十分简单的.首先,从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;
  }];
}

上面的方法创建一个signal,用于采用当前username和password登录.现在对其中的部分进行详解.

上面的代码使用RACSignalcreateSignal:方法创建了signal. 有一个block参数.当此signal拥有一个订阅者时,block中的内部代码就会执行.

block有传入一个实现了RACSubscriber协议的subscriber实例,它拥有一个你可以调用发送事件的方法;你可能会发送多个next事件,因为errorcomplete终止的事件.关于这点,它发送一个单独next事件,来表示登录是否成功,紧接着调用complete事件.block会返回一个RACDisposable类型的对象,它允许在一个订阅被取消或者销毁时,执行必要的清理工作.此signal没有任何的清理需求,所以返回nil.

正如你所看到的,封装一个异步API打牌singal中是多么的简单!
现在使用这个新的signal.更新之前viewDidLoad方法最后面的代码为下面:

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

上面的代码使用map方法,将按钮的touch signal 转换成登录逻辑的signal.现在subscriber就是执行了个简单的打印操作.如果你运行app,点击登录按钮,看一下Xcode控制台,你会看到打印的输出...
...打印的结果可能和你想的不一样!

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

The subscribeNext: block has been passed a signal all right, but not the result of the sign-in signal!
Time to illustrate this pipeline so you can see what’s going on:

subscribeNext:block接收到了一个signal参数,但却不是登录signal!现在说明一下本次的管道内容:

15230636542046.png

rac_signalForControlEvents会在你点击登录按钮时发送一个next事件(将UIButton对象作为事件的数据).map方法会创建并返回一个登录signal,也就是说在此步骤之后的管道操作都接收RACSignal.你要特别注意subscribeNext:这一步操作.

上面的这种情况叫做signal of signals;也就是说外部signal包含一个内部signal.如果你要这么多,你可以在外部signal的subscribeNext:block中订阅内部signal.但是它现在的返回的很混乱.幸运的是,这是一个普遍性问题
,ReactiveCocoa已经针对这个做了处理.

Signal of Signals

The solution to this problem is straightforward, just change the map step to a flattenMap step as shown below:
解决这个问题还是很简单的,只是将map操作换成flattenMap,就像下面展示的那样:

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

这段代码会将之前的按钮touch事件映射到一个登录signal上,同时也会将事件从内部signal发送到外部signal.

运行app,不要走神.控制台会打印登录是否成功:

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:block中,来执行成功登楼后的页面跳转.使用以下代码替换管道相应的步骤:

[[[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:block会接受登录signal的结果,相应的更新signInFailureText text field的显示,并根据结果执行navigation segue操作.

运行app,登录将会看到一个可爱的小猫咪!

你有注意到在当前的app有点小小的影响用体验的问题吗?当sign-inservice对象开始验证时,登录按钮应该在失效状态.这可以语法用户重复点击登录按钮.而且如果登录失败了,错误新应该在用户重新登录时隐藏掉.

但你应该如何将此逻辑添加到当前的管道中呢?改变按钮的有效性并不似一种转换,filter或者你之前所接触到的任何一种概念.相反,这是一个已知的副作用;或许你想在管道的next事件发生时,在内部执行对应的逻辑,但这并不会实际地改变事件本身.

Adding 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:操作步骤添加到了管道的按钮touch事件创建步骤之后.注意doNext:中的block并没有返回值(block参数的返回值),因为他是一个副作用;它保持事件本身不变.
doNext:block中设置了按钮无效,并隐藏提示文字.同时subscribeNext:block中,恢复按钮有效,并根据登录的结果显示提示文字.
该更新一下包含该副作用的管道图了:

15231099021559.png

运行app,测试登录按钮是否和期望的那样有效或失效.
你的任务完成了 - 整个app都是响应式了.
如果你对此有些迷惑,你可以下载最终项目(以添加过依赖),或者直接从GitHub上获得代码,教程中每个讲解都被提交了commit.

提示:在一些异步操作在进行中时禁用按钮也是一个普遍性问题,ReactiveCocoa再一次被这个小陷阱所笼罩.RACCommand封装了这个概念,并有一个enabled的signal,允许您将按钮的enabled属性连接到信号.你可以使用此类尝试一下.

Conclusions

希望这份指南能帮助你开始在你自己的项目中使用ReactiveCocoa这个框架.使用这些概念做一些练习,但是任何编程语言,一旦你掌握了原理,都会非常简单.在ReactiveCocoa中最核心的概念就是signal,它就是一些事件流.还有什么比这更简单的呢?

ReactiveCocoa其中的一个优点就是可以使用几种不同的方式来解决相同的问题.你可能想使用此app来做实验,通过分离和合并来调整管道和signal.

ReactiveCocoa最有价值的地方就是让你的代码变得更清晰,更容易让人理解.个人觉得如果app的逻辑管道很清晰,使用链式编程会更容易理解.

这次教程的第二部分,你将会学习到更多的主题,比如error处理和如何管理在不同线程中执行的代码.Until then, have fun experimenting!

上一篇 下一篇

猜你喜欢

热点阅读