ReactiveCocoa入门教程——第一部分
作为一个iOS开发者,你写的每一行代码几乎都是在响应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。 如果你对上面说的还比较疑惑,那还是继续往下看吧。 ReactiveCocoa结合了几种编程风格: 函数式编程( 响应式编程( 所以,你可能听说过ReactiveCocoa被描述为函数响应式编程(FRP)框架。 这就是这篇教程要讲的内容。编程范式是个不错的主题,但是本篇教程的其余部分将会通过一个例子来实践。 Reactive Playground通过这篇教程,一个简单的范例应用Reactive Playground ,你将会了解到响应式编程。下载初始工程,然后编译运行一下确保你已经把一切都设置正确了。 Reactive Playground是一个非常简单的应用,它为用户展示了一个登录页。在用户名框输入user,在密码框输入password,然后你就能看到有一只可爱小猫咪的欢迎页了。 呀,真是可爱啊。 打开 使用ReactiveCocoa,可以使应用的基本逻辑变得相当简洁。是时候开始啦。 添加ReactiveCocoa框架添加ReactiveCocoa框架最简单的方法就是用CocoaPods。如果你从没用过CocoaPods,那还是先去看看CocoaPods简介这篇教程吧。请至少看完教程中初始化的步骤,这样你才能安装框架。 注意:如果不想用CocoaPods,你仍然可以使用ReactiveCocoa,具体查看Github文档中引入ReactiveCocoa的步骤描述。 译注:我就是不喜欢用CocoaPods的那波人。所以我首先使用了Github上提供的方法,但是在第二步执行bootstrap时提示缺少xctool,我就果断放弃了,还是乖乖用CocoaPods吧。 具体怎么使用CocoaPods安装就不详细讲解了。 开动就像在介绍中提到的,RAC为应用中发生的不同事件流提供了一个标准接口。在ReactiveCocoa术语中这个叫做信号(signal),由 打开应用的初始view controller, #import <ReactiveCocoa/ReactiveCocoa.h>
不要替换已有的代码,将下面的代码添加到 [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?
可以看到每次改变文本框中的文字,block中的代码都会执行。没有target-action,没有delegate,只有signal和block。令人激动不是吗? ReactiveCocoa框架使用category来为很多基本UIKit控件添加signal。这样你就能给控件添加订阅了,text field的 原理就说这么多,是时候开始让ReactiveCocoa干活了。 ReactiveCocoa有很多操作来控制事件流。假设你只关心超过3个字符长度的用户名,那么你可以使用 [[self.usernameTextField.rac_textSignal
filter:^BOOL(id value){
NSString*text = value;
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@",x);
}];
编译运行,在text field只能怪输入几个字,你会发现只有当输入超过3个字符时才会有log。 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?
刚才所创建的只是一个很简单的管道。这就是响应式编程的本质,根据数据流来表达应用的功能。 用图形来表达就是下面这样的:
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);
}];
类型转换如果你之前把代码分成了多个步骤,现在再把它改回来吧。。。。。。。。 [[self.usernameTextField.rac_textSignal
filter:^BOOL(id value){
NSString*text = value; // implicit cast
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@",x);
}];
在上面的代码中,注释部分标记了将 [[self.usernameTextField.rac_textSignal
filter:^BOOL(NSString*text){
return text.length > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@",x);
}];
编译运行,确保没什么问题。 什么是事件呢?到目前为止,本篇教程已经描述了不同的事件类型,但是还没有说明这些事件的结构。有意思的是(?),事件可以包括任何事情。 下面来展示一下,在管道中添加另一个操作。把添加在 [[[self.usernameTextField.rac_textSignal
map:^id(NSString*text){
return @(text.length);
}]
filter:^BOOL(NSNumber*length){
return[length integerValue] > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@",x);
}];
编译运行,你会发现log输出变成了文本的长度而不是内容。 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
新加的 来看下面的图片:
现在差不多是时候用所学的内容来更新一下 创建有效状态信号首先要做的就是创建一些信号,来表示用户名和密码输入框中的输入内容是否有效。把下面的代码添加到 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]);
}];
可以看到,上面的代码对每个输入框的 下一步是转换这些信号,从而能为输入框设置不同的背景颜色。基本上就是,你订阅这些信号,然后用接收到的值来更新输入框的背景颜色。下面有一种方法: [[validPasswordSignal
map:^id(NSNumber *passwordValid){
return[passwordValid boolValue] ? [UIColor clearColor]:[UIColor yellowColor];
}]
subscribeNext:^(UIColor *color){
self.passwordTextField.backgroundColor = color;
}];
从概念上来说,就是把之前信号的输出应用到输入框的 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];
}];
你不觉得这种方法很好吗? 在编译运行之前,找到 self.usernameTextField.backgroundColor =
self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor];
self.passwordTextField.backgroundColor =
self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];
这样就把不相关的代码删掉了。 编译运行,可以发现当输入内容无效时,输入框看起来高亮了,有效时又透明了。 现在的逻辑用图形来表示就是下面这样的。能看到有两条简单的管道,两个文本信号,经过一个map转为表示是否有效的布尔值,再经过一个map转为 你是否好奇为什么要创建两个分开的
聚合信号目前在应用中,登录按钮只有当用户名和密码输入框的输入都有效时才工作。现在要把这里改成响应式的。 现在的代码中已经有可以产生用户名和密码输入框是否有效的信号了—— 把下面的代码添加到 RACSignal *signUpActiveSignal =
[RACSignal combineLatest:@[validUsernameSignal,validPasswordSignal]
reduce:^id(NSNumber*usernameValid,NSNumber *passwordValid){
return @([usernameValid boolValue]&&[passwordValid boolValue]);
}];
上面的代码使用
现在已经有了合适的信号,把下面的代码添加到 [signUpActiveSignal subscribeNext:^(NSNumber*signupActive){
self.signInButton.enabled =[signupActive boolValue];
}];
在运行之前,把以前的旧实现删掉。把下面这两个属性删掉。 @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];
同样把 最后确保把 现在应用的逻辑就是下面这样的: 上图展示了一些重要的概念,你可以使用ReactiveCocoa来完成一些重量级的任务。
这些改动的结果就是,代码中没有用来表示两个输入框有效状态的私有属性了。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。 响应式的登录应用目前使用上面图中展示的响应式管道来管理输入框和按钮的状态。但是按钮按下的处理用的还是action,所以下一步就是把剩下的逻辑都替换成响应式的。 在storyboard中,登录按钮的 打开 现在回到 [[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
NSLog(@"button clicked");
}];
上面的代码从按钮的 编译运行,确保的确有log输出。按钮只在用户名和密码框输入有效时可用,所以在点击按钮前需要在两个文本框中输入一些内容。 可以看到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
现在按钮有了点击事件的信号,下一步就是把它和登录流程连接起来。那么问题就来了,打开 typedef void (^RWSignInResponse)(BOOL);
@interface RWDummySignInService : NSObject
- (void)signInWithUsername:(NSString *)username
password:(NSString *)password
complete:(RWSignInResponse)completeBlock;
@end
这个service有3个参数,用户名、密码和一个完成回调block。这个block会在登录成功或失败时执行。你可以在按钮点击事件的
创建信号幸运的是,把已有的异步API用信号的方式来表示相当简单。首先把 还是在 - (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;
}];
}
上面的方法创建了一个信号,使用用户名和密码登录。现在分解来看一下。 block的入参是一个 这个block的返回值是一个 可以看到,把一个异步API用信号封装是多简单! 现在就来使用这个新的信号。把之前添加在viewDidLoad中的代码更新成下面这样的: [[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
map:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(id x){
NSLog(@"Sign in result: %@",x);
}];
上面的代码使用 编译运行,点击登录按钮,查看Xcode的控制台,等等,输出的这是个什么鬼? 2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign in result:
<RACDynamicSignal: 0xa068a00> name: +createSignal:
没错,你已经给 上面问题的解决方法,有时候叫做信号中的信号,换句话说就是一个外部信号里面还有一个内部信号。你可以在外部信号的 信号中的信号解决的方法很简单,只需要把 [[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
flattenMap:^id(id x){
return[self signInSignal];
}]
subscribeNext:^(id x){
NSLog(@"Sign in result: %@",x);
}];
这个操作把按钮点击事件转换为登录信号,同时还从内部信号发送事件到外部信号。 编译运行,注意控制台,现在应该输出登录是否成功了。 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
还不错。 现在已经完成了大部分的内容,最后就是在 [[[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];
}
}];
编译运行,应该就能再看到可爱的小猫啦!喵~ 你注意到这个应用现在有一些用户体验上的小问题了吗?当登录service正在校验用户名和密码时,登录按钮应该是不可点击的。这会防止用户多次执行登录操作。还有,如果登录失败了,用户再次尝试登录时,应该隐藏错误信息。 这个逻辑应该怎么添加呢?改变按钮的可用状态并不是转换(map)、过滤(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];
}
}];
你可以看到 上面的 之前的管道图就更新成下面这样的: 现在所有的工作都已经完成了,这个应用已经是响应式的啦。 如果你中途哪里出了问题,可以下载最终的工程(依赖库都有),或者在Github上找到这份代码,教程中的每一次编译运行都有对应的commit。
总结希望本教程为你今后在自己的应用中使用ReactiveCocoa打下了一个好的基础。你可能需要一些练习来熟悉这些概念,但就像是语言或者编程,一旦你夯实基础,用起来也就很简单了。ReactiveCocoa的核心就是信号,而它不过就是事件流。还能再更简单点吗? 在使用ReactiveCocoa后,我发现了一个有趣的事情,那就是你可以用很多种不同的方法来解决同一个问题。你可以用教程中的例子试试,调整一下信号,改改信号的分割和聚合。 ReactiveCocoa的主旨是让你的代码更简洁易懂,这值得多想想。我个人认为,如果逻辑可以用清晰的管道、流式语法来表示,那就很好理解这个应用到底干了什么了。 在本系列教程的第二部分,你将会学到诸如错误处理、在不同线程中执行代码等高级用法。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |