ReactiveCocoa入门教程

第一部分

本文翻译自RayWenderlich,原文:ReactiveCocoa Tutorial--The Definitive Introduction: Part 1/2

作为一个iOS开发者,你写的每一行代码几乎都是在相应某个事件,例如按钮的点击,收到网络消息,属性的变化(通过KVO)或者用户位置的变化(通过CoreLocation)。但是这些事件都用不同的方式来处理,比如action、delegate、KVO、callback等。ReactiveCocoa为事件定义了一个标准接口,从而可以使用一些基本工具来更容易的连接、过滤和组合。

如果你对上面说的还比较疑惑,那还是继续往下看吧。

ReactiveCocoa结合了几种编程风格:

函数式编程(Functional Programming):使用高阶函数,例如函数用其他函数作为参数。

响应式编程(Reactive Programming):关注于数据流和变化传播。

所以,你可能听说过ReactiveCocoa被描述为函数响应式编程(FRP)框架。

这就是这篇教程要讲的内容。编程范式是个不错的主题,但是本篇教程的其余部分将会通过一个例子来实践。 

Reactive Playground

通过这篇教程,一个简单的范例应用Reactive Playground,你将会了解到响应式编程。下载初始工程,然后编译运行一下确保你已经把一切都设置正确了。

Reactive Playground是一个非常简单的应用,它为用户展示了一个登录页。在用户名框输入user,在密码框输入password,然后你就能看到有一只可爱小猫咪的欢迎页了。

01.jpg

呀,真是可爱啊。

现在可以花一些时间来看一下初始工程的代码。很简单,用不了多少时间。

打开RWViewController.m看一下。你多快能找到控制登录按钮是否可用的条件?判断显示/隐藏登录失败label的条件是什么?在这个相对简单的例子里,可能只用一两分钟就能回答这些问题。但是对于更复杂的例子,这些所花的时间可能就比较多了。

使用ReactiveCocoa,可以使应用的基本逻辑变得相当简洁。是时候开始啦。

添加ReactiveCocoa框架

添加ReactiveCocoa框架最简单的方法就是用CocoaPods。如果你从没用过CocoaPods,那还是先去看看CocoaPods简介这篇教程吧。请至少看完教程中初始化的步骤,这样你才能安装框架。

注意:如果不想用CocoaPods,你仍然可以使用ReactiveCocoa,具体查看Github文档中引入ReactiveCocoa的步骤描述。

译注:我就是不喜欢用CocoaPods的那波人。所以我首先使用了Github上提供的方法,但是在第二步执行bootstrap时提示缺少xctool,我就果断放弃了,还是乖乖用CocoaPods吧。

具体怎么使用CocoaPods安装就不详细讲解了。 

开始

就像在介绍中提到的,RAC为应用中发生的不同事件流提供了一个标准接口。在ReactiveCocoa术语中这个叫做信号(signal),由RACSignal类表示。

打开应用的初始view controller,RWViewController.m ,引入ReactiveCocoa的头文件。

1
#import

不要替换已有的代码,将下面的代码添加到viewDidLoad方法的最后:

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

 编译运行,在用户名输入框中输几个字。注意console的输出应该和下面的类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 signal(RACSignal)发送事件流给它的subscriber。目前总共有三种类型的事件:next、error、completed。一个signal在因error终止或者完成前,可以发送任意数量的next事件。在本教程的第一部分,我们将会关注next事件。在第二部分,将会学习error和completed事件。

RACSignal有很多方法可以来订阅不同的事件类型。每个方法都需要至少一个block,当事件发生时就会执行block中的逻辑。在上面的例子中可以看到每次next事件发生时,subscribeNext:方法提供的block都会执行。

ReactiveCocoa框架使用category来为很多基本UIKit控件添加signal。这样你就能给控件添加订阅了,text field的rac_textSignal就是这么来的。

原理就说这么多,是时候开始让ReactiveCocoa干活了。

ReactiveCocoa有很多操作来控制事件流。假设你只关心超过3个字符长度的用户名,那么你可以使用filter操作来实现这个目的。把之前加在viewDidLoad中的代码更新成下面的:

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

编译运行,在text field只能怪输入几个字,你会发现只有当输入超过3个字符时才会有log。

1
2
3
4
5
6
7
8
9
10
11
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?

刚才所创建的只是一个很简单的管道。这就是响应式编程的本质,根据数据流来表达应用的功能。

用图形来表达就是下面这样的:

FilterPipeline.png

从上面的图中可以看到,rac_textSignal是起始事件。然后数据通过一个filter,如果这个事件包含一个长度超过3的字符串,那么该事件就可以通过。管道的最后一步就是subscribeNext:,block在这里打印出事件的值。

filter操作的输出也是RACSignal,这点先放到一边。你可以像下面那样调整一下代码来展示每一步的操作。

1
2
3
4
5
6
7
8
9
10
11
12
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大量使用block。如果你是block新手,你可能想看看Apple官方的block编程指南。如果你熟悉block,但是觉得block的语法有些奇怪和难记,你可能会想看看这个有趣又实用的网页f*****gblocksyntax.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);
   }];

在上面的代码中,注释部分标记了将id隐式转换为NSString,这看起来不是很好看。幸运的是,传入block的值肯定是个NSString,所以你可以直接修改参数类型,把代码更新成下面的这样的:

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

编译运行,确保没什么问题。

什么是事件呢?

到目前为止,本篇教程已经描述了不同的事件类型,但是还没有说明这些事件的结构。有意思的是(?),事件可以包括任何事情。

下面来展示一下,在管道中添加另一个操作。把添加在viewDidLoad中的代码更新成下面的:

1
2
3
4
5
6
7
8
9
10
[[[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输出变成了文本的长度而不是内容。

1
2
3
4
5
6
7
8
9
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改变了事件的数据。map从上一个next事件接收数据,通过执行block把返回值传给下一个next事件。在上面的代码中,map以NSString为输入,取字符串的长度,返回一个NSNumber。

来看下面的图片:

FilterAndMapPipeline.png

能看到map操作之后的步骤收到的都是NSNumber实例。你可以使用map操作来把接收的数据转换成想要的类型,只要它是个对象。

注意:在上面的例子中text.length返回一个NSUInteger,是一个基本类型。为了将它作为事件的内容,NSUInteger必须被封装。幸运的是Objective-C literal syntax提供了一种简单的方法来封装——@ (text.length)。

现在差不多是时候用所学的内容来更新一下ReactivePlayground应用了。你可以把之前的添加代码都删除了。

创建有效状态信号

首先要做的就是创建一些信号,来表示用户名和密码输入框中的输入内容是否有效。把下面的代码添加到RWViewController.m中viewDidLoad的最后面:

1
2
3
4
5
6
7
8
9
10
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转换。输出是一个用NSNumber封装的布尔值。

下一步是转换这些信号,从而能为输入框设置不同的背景颜色。基本上就是,你订阅这些信号,然后用接收到的值来更新输入框的背景颜色。下面有一种方法:

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

(不要使用这段代码,下面有一种更好的写法!)

从概念上来说,就是把之前信号的输出应用到输入框的backgroundColor属性上。但是上面的用法不是很好。

幸运的是,ReactiveCocoa提供了一个宏来更好的完成上面的事情。把下面的代码直接加到viewDidLoad中两个信号的代码后面:

1
2
3
4
5
6
7
8
9
10
11
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宏允许直接把信号的输出应用到对象的属性上。RAC宏有两个参数,第一个是需要设置属性值的对象,第二个是属性名。每次信号产生一个next事件,传递过来的值都会应用到该属性上。

你不觉得这种方法很好吗?

在编译运行之前,找到updateUIState方法,把头两行删掉。

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

这样就把不相关的代码删掉了。

编译运行,可以发现当输入内容无效时,输入框看起来高亮了,有效时又透明了。

现在的逻辑用图形来表示就是下面这样的。能看到有两条简单的管道,两个文本信号,经过一个map转为表示是否有效的布尔值,再经过一个map转为UIColor,而这个UIColor已经和输入框的背景颜色绑定了。

TextFieldValidPipeline.png

你是否好奇为什么要创建两个分开的validPasswordSignal和validUsernameSignal呢,而不是每个输入框一个单独的管道呢?(?)稍安勿躁,答案就在下面。

原文:Are you wondering why you created separate validPasswordSignal and validUsernameSignal signals, as opposed to a single fluent pipeline for each text field? Patience dear reader, the method behind this madness will become clear shortly!

聚合信号

目前在应用中,登录按钮只有当用户名和密码输入框的输入都有效时才工作。现在要把这里改成响应式的。

现在的代码中已经有可以产生用户名和密码输入框是否有效的信号了——validUsernameSignal和validPasswordSignal了。现在需要做的就是聚合这两个信号来决定登录按钮是否可用。

把下面的代码添加到viewDidLoad的末尾:

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

上面的代码使用combineLatest:reduce:方法把validUsernameSignal和validPasswordSignal产生的最新的值聚合在一起,并生成一个新的信号。每次这两个源信号的任何一个产生新值时,reduce block都会执行,block的返回值会发给下一个信号。

注意:RACsignal的这个方法可以聚合任意数量的信号,reduce block的参数和每个源信号相关。ReactiveCocoa有一个工具类RACBlockTrampoline,它在内部处理reduce block的可变参数。实际上在ReactiveCocoa的实现中有很多隐藏的技巧,值得你去看看。

现在已经有了合适的信号,把下面的代码添加到viewDidLoad的末尾。这会把信号和按钮的enabled属性绑定。

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

在运行之前,把以前的旧实现删掉。把下面这两个属性删掉。

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

把viewDidLoad中的这些也删掉:

1
2
3
4
5
6
7
// 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的调用删掉。

编译运行,看看登录按钮。当用户名和密码输入有效时,按钮就是可用的,和以前一样。

现在应用的逻辑就是下面这样的:

CombinePipeline.png

上图展示了一些重要的概念,你可以使用ReactiveCocoa来完成一些重量级的任务。

  • 分割——信号可以有很多subscriber,也就是作为很多后续步骤的源。注意上图中那个用来表示用户名和密码有效性的布尔信号,它被分割成多个,用于不同的地方。

  • 聚合——多个信号可以聚合成一个新的信号,在上面的例子中,两个布尔信号聚合成了一个。实际上你可以聚合并产生任何类型的信号。

这些改动的结果就是,代码中没有用来表示两个输入框有效状态的私有属性了。这就是用响应式编程的一个关键区别,你不需要使用实例变量来追踪瞬时状态。

响应式的登录

应用目前使用上面图中展示的响应式管道来管理输入框和按钮的状态。但是按钮按下的处理用的还是action,所以下一步就是把剩下的逻辑都替换成响应式的。

在storyboard中,登录按钮的Touch Up Inside事件和RWViewController.m中的signInButtonTouched方法是绑定的。下面会用响应的方法替换,所以首先要做的就是断开当前的storyboard action。

打开Main.storyboard,找到登录按钮,按住ctrl键单击,打开outlet/action连接框,然后点击x来断开连接。如果你找不到的话,下图中红色箭头指示的就是删除按钮。

DisconnectAction.jpg

你已经知道了ReactiveCocoa框架是如何给基本UIKit控件添加属性和方法的了。目前你已经使用了rac_textSignal,它会在文本发生变化时产生信号。为了处理按钮的事件,现在需要用到ReactiveCocoa为UIKit添加的另一个方法,rac_signalForControlEvents。

现在回到RWViewController.m,把下面的代码添加到viewDidLoad的末尾:

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

上面的代码从按钮的UIControlEventTouchUpInside事件创建了一个信号,然后添加了一个订阅,在每次事件发生时都会输出log。

编译运行,确保的确有log输出。按钮只在用户名和密码框输入有效时可用,所以在点击按钮前需要在两个文本框中输入一些内容。

可以看到Xcode控制台的输出和下面的类似:

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

现在按钮有了点击事件的信号,下一步就是把它和登录流程连接起来。那么问题就来了,打开RWDummySignInService.h,看一下接口:

1
2
3
4
5
6
7
8
9
typedef void (^RWSignInResponse)(BOOL);
  
@interface RWDummySignInService : NSObject
  
- (void)signInWithUsername:(NSString *)username
                   password:(NSString *)password
                   complete:(RWSignInResponse)completeBlock;
                   
@end

这个service有3个参数,用户名、密码和一个完成回调block。这个block会在登录成功或失败时执行。你可以在按钮点击事件的subscribeNext: blcok里直接调用这个方法,但是为什么你要这么做?(?)

注意:本教程为了简便使用了一个假的service,所以它不依赖任何外部API。但你现在的确遇到了一个问题,如何使用这些不是用信号表示的API呢?

创建信号

幸运的是,把已有的异步API用信号的方式来表示相当简单。首先把RWViewController.m中的signInButtonTouched:删掉。你会用响应式的的方法来替换这段逻辑。

还是在RWViewController.m中,添加下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
- (RACSignal *)signInSignal {
return  [RACSignal createSignal:^RACDisposable *(id subscriber){
    [self.signInService 
      signInWithUsername:self.usernameTextField.text
                password:self.passwordTextField.text
                complete:^(BOOL success){
                     [subscriber sendNext:@(success)];
                     [subscriber sendCompleted];
      }];
    return  nil;
}];
}

上面的方法创建了一个信号,使用用户名和密码登录。现在分解来看一下。

上面的代码使用RACSignal的createSignal:方法来创建信号。方法的入参是一个block,这个block描述了这个信号。当这个信号有subscriber时,block里的代码就会执行。

block的入参是一个subscriber实例,它遵循RACSubscriber协议,协议里有一些方法来产生事件,你可以发送任意数量的next事件,或者用error\complete事件来终止。本例中,信号发送了一个next事件来表示登录是否成功,随后是一个complete事件。

这个block的返回值是一个RACDisposable对象,它允许你在一个订阅被取消时执行一些清理工作。当前的信号不需要执行清理操作,所以返回nil就可以了。

可以看到,把一个异步API用信号封装是多简单!

现在就来使用这个新的信号。把之前添加在viewDidLoad中的代码更新成下面这样的:

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

上面的代码使用map方法,把按钮点击信号转换成了登录信号。subscriber输出log。

编译运行,点击登录按钮,查看Xcode的控制台,等等,输出的这是个什么鬼?

1
2
2014-01-08 21:00:25.919 RWReactivePlayground[33818:a0b] Sign  in  result:
                                     name: +createSignal:

没错,你已经给subscribeNext:的block传入了一个信号,但传入的不是登录结果的信号。

下图展示了到底发生了什么:

02.png

当点击按钮时,rac_signalForControlEvents发送了一个next事件(事件的data是UIButton)。map操作创建并返回了登录信号,这意味着后续步骤都会收到一个RACSignal。这就是你在subscribeNext:这步看到的。

上面问题的解决方法,有时候叫做信号中的信号,换句话说就是一个外部信号里面还有一个内部信号。你可以在外部信号的subscribeNext:block里订阅内部信号。不过这样嵌套太混乱啦,还好ReactiveCocoa已经解决了这个问题。

信号中的信号

解决的方法很简单,只需要把map操作改成flattenMap就可以了:

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

这个操作把按钮点击事件转换为登录信号,同时还从内部信号发送事件到外部信号。

编译运行,注意控制台,现在应该输出登录是否成功了。

1
2
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
[[[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从登录信号中取得结果,相应地更新signInFailureText是否可见。如果登录成功执行导航跳转。

编译运行,应该就能再看到可爱的小猫啦!喵~

03.jpg

你注意到这个应用现在有一些用户体验上的小问题了吗?当登录service正在校验用户名和密码时,登录按钮应该是不可点击的。这会防止用户多次执行登录操作。还有,如果登录失败了,用户再次尝试登录时,应该隐藏错误信息。

这个逻辑应该怎么添加呢?改变按钮的可用状态并不是转换(map)、过滤(filter)或者其他已经学过的概念。其实这个就叫做“副作用”,换句话说就是在一个next事件发生时执行的逻辑,而该逻辑并不改变事件本身。

添加附加操作(Adding side-effects)

把代码更新成下面的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[[[[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: block并没有返回值。因为它是附加操作,并不改变事件本身。

上面的doNext: block把按钮置为不可点击,隐藏登录失败提示。然后在subscribeNext: block里重新把按钮置为可点击,并根据登录结果来决定是否显示失败提示。

之前的管道图就更新成下面这样的:

04.png

编译运行,确保登录按钮的可点击状态和预期的一样。

现在所有的工作都已经完成了,这个应用已经是响应式的啦。

如果你中途哪里出了问题,可以下载最终的工程(依赖库都有),或者在Github上找到这份代码,教程中的每一次编译运行都有对应的commit。

注意:在异步操作执行的过程中禁用按钮是一个常见的问题,ReactiveCocoa也能很好的解决。RACCommand就包含这个概念,它有一个enabled信号,能让你把按钮的enabled属性和信号绑定起来。你也许想试试这个类。

总结

希望本教程为你今后在自己的应用中使用ReactiveCocoa打下了一个好的基础。你可能需要一些练习来熟悉这些概念,但就像是语言或者编程,一旦你夯实基础,用起来也就很简单了。ReactiveCocoa的核心就是信号,而它不过就是事件流。还能再更简单点吗?

在使用ReactiveCocoa后,我发现了一个有趣的事情,那就是你可以用很多种不同的方法来解决同一个问题。你可以用教程中的例子试试,调整一下信号,改改信号的分割和聚合。

ReactiveCocoa的主旨是让你的代码更简洁易懂,这值得多想想。我个人认为,如果逻辑可以用清晰的管道、流式语法来表示,那就很好理解这个应用到底干了什么了。

在本系列教程的第二部分,你将会学到诸如错误处理、在不同线程中执行代码等高级用法。

第二部分

翻译自:http://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt2

ReactiveCocoa 是一个框架,它允许你在你的iOS程序中使用函数响应式(FRP)技术。加上第一部分的讲解,你将会学会如何使用信号量(对事件发出数据流)如何替代标准的动作和事件处理逻辑。你也会学到如何转换、分离和组合这些信号量。

在这里,也就是第二部分里,你将会学到更多先进的ReactiveCocoa特性,包括:

1、另外两个事件类型:error和completed

2、Throttling(节流

3、Threading

4、Continuations

5、更多。。。

是时候开始了。

Twitter Instant

这里我们要使用的贯穿整个教程的程序是叫做Twitter Instant的程序,该程序可以在你输入的时候实时更新搜索到的结果。

该应用包括一些基本的用户交互界面和一些平凡的代码,了解之后就可以开始了。在第一部分里面,你使用Cocoapods来把CocoaPods加载到你的工程里面,这里的工程里面就已经包含了Podfile文件,你只需要pod install一下即可。

然后重新打开工程即可。(这个时候打开TwitterInstant.xcworkspace):

1、TwitterInstant:这是你的程序逻辑

2、Pods:里面是包括的三方类库

运行一下程序,你会看到如下的页面:

Simulator Screen Shot 2016年1月16日 上午11.28.37.png

花费一会时间让你自己熟悉一下整个工程。它就是一个简单的split viewController app.左边的是RWSearchFormViewController,右边的是:RWSearchResultsViewController。

自己说:原文简单介绍了一下该工程,就不在介绍看一下就可以了。

验证搜索文本

你第一件要做的事情就是去验证一下搜索文本,让它确保大于两个字符串。如果你看了第一篇文章,这个将会很简单。

在RWSearchFormViewController.m中添加方法如下:

1
2
3
- (BOOL)isValidSearchText:(NSString *)text {
   return  text.length >  2 ;
}

这就简单的保证了搜索的字符串大于两个字符。写这个很简单的逻辑你可能会问:为什么要分开该方法到工程文件里面呢?

当前的逻辑很简单,但是如果后面这个会更复杂呢?在上面的例子中,你只需要修改一个地方。此外,上面的写法让你的代码更有表现力,它告诉你为什么要检查string的长度。我们应该遵守好的编码习惯,不是么?

然后,我们导入头文件:

1
# import

然后在导入该头文件的文件里面的viewDidLoad后面写上如下代码:

1
2
3
4
5
[[self.searchText.rac_textSignal
   map:^id(NSString *text) {
     return  [self isValidSearchText:text] ?      [UIColor whiteColor] : [UIColor yellowColor];  }]
   subscribeNext:^(UIColor *color) {
     self.searchText.backgroundColor = color;  }];

想想这是做什么呢?上面的代码:

1、取走搜索文本框的信号量

2、把它转换一下:用背景色来预示内容是否可用。

3、然后设置backgroundColor属性在subscribeNext:的block里面。

Build然后运行我们就会发现当搜索有效的时候就会是白色,搜索字符串无效的时候就是黄色。

下面是图解,这个简单的反应传输看起来如下:

TextValidationPipeline.png

ran_textSignal发出包含当前文本框每次改变内容的next事件。map那个步骤转换text value,将其转换成了color,subscribeNext那一步将这个value提供给了textField的background。

当然了,你从第一个教程一定记得这些,对吧?如果你不记得的话,你也许想在这里停止阅读,至少读了整个测试工程。

在添加Twitter 搜索逻辑之前 ,这里有一些更有趣的话题。

Formatting of Pipelines

当你正在钻研格式化的ReactiveCocoa代码的时候,普遍接受的惯例就是:每一个操作在一个新行,和所有步骤垂直对齐的。

在下面的图片,你会看到更复杂的对齐方式,从上一个教程拿来的图片:

PipelineFormatting.png

这样你会更容易看到该组成管道的操作。另外,在每个block中用最少的代码任何超过几行的都应该拆分出一个私有的方法。

不幸的是,Xcode真的不喜欢这种格式类型的代码,因此你可能需要找到自己调整。

Memory Management

思考一下你刚才加入到TwitterInstant的代码。你是否想过你刚才创建的管道式如何保留的呢?无疑地,是否是它没有赋值为一个变量或者属性他就不会有自己的引用计数,注定会消亡呢?

其中一个设计目标就是ReactiveCocoa允许这种类型的编程,这里管道可以匿名形式。所有你写过的响应式代码都应该看起来比较直观。

为了支持这种模型,ReactiveCocoa维持和保留自己全局的信号。如果它有一个或者多个subscribers(订阅者),信号就会活跃。如果所有的订阅者都移除掉了,信号就会被释放。想了解更多关于ReactiveCocoa管理进程,可以参看Memory Management 文档。

这就剩下了最后的问题:你如何从一个信号取消订阅?当一个completed或者error事件之后,订阅会自动的移除(一会就会学到)。手工的移除将会通过RACDisposable.

所有RACSignal的订阅方法都会返回一个RACDisposable实例,它允许你通过处置方法手动的移除订阅。下面是一个使用当前管道的快速的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RACSignal *backgroundColorSignal =
   [self.searchText.rac_textSignal
     map:^id(NSString *text) {
       return  [self isValidSearchText:text] ?
         [UIColor whiteColor] : [UIColor yellowColor];
     }];
  
RACDisposable *subscription =
   [backgroundColorSignal
     subscribeNext:^(UIColor *color) {
       self.searchText.backgroundColor = color;
     }];
  
// at some point in the future ...
[subscription dispose];

你不会经常做这些,但是你必须知道可能性的存在。

Note:作为这些的一个推论,如果你创建了一个管道,但是你不给他订阅,这个管道将不会执行,这些包括任何侧面的影响,例如doNext:blocks。

Avoiding Retain Cycles

当ReactiveCocoa在场景背后做了好多聪明的事情—这就意味着你不必要担心太多关于信号量的内存管理——这里有一个很重要的内存喜爱那个管的问你你需要考虑。

如果你看到下面的响应式代码你仅仅加入:

1
2
3
4
5
6
7
8
[[self.searchText.rac_textSignal
   map:^id(NSString *text) {
     return  [self isValidSearchText:text] ?
       [UIColor whiteColor] : [UIColor yellowColor];
   }]
   subscribeNext:^(UIColor *color) {
     self.searchText.backgroundColor = color;
   }];

subscribeNext:block使用self来获得一个textField的引用,Blocks在封闭返回内捕获并且持有了值。因此在self和这个信号量之间造成了强引用,造成了循环引用。这取决于对象的生命周期,如果他的生命周期是应用程序的生命周期,那这样是没关系的,但是在更复杂的应用中就不行了。

为了避免这种潜在的循环引用,苹果官方文档:Working With Blocks 建议捕捉一个弱引用self,当前的代码可以这样写:

1
2
3
4
5
6
7
8
9
10
__weak RWSearchFormViewController *bself = self;  // Capture the weak reference
  
[[self.searchText.rac_textSignal
   map:^id(NSString *text) {
     return  [self isValidSearchText:text] ?
       [UIColor whiteColor] : [UIColor yellowColor];
   }]
   subscribeNext:^(UIColor *color) {
     bself.searchText.backgroundColor = color;
   }];

在上面的代码中,bself就是self标记为__weak(使用它可以make一个弱引用)的引用,现在可以看到使用textField的时候使用bself代用的。这看起来并不是那么高雅。

ReactiveCocoa框架包含了一个小诀窍,你可以使用它代替上百年的代码。添加下面的引用:

1
# import  "RACEXTScope.h"

然后代码修改后如下:

1
2
3
4
5
6
7
8
9
10
@weakify (self)
[[self.searchText.rac_textSignal
   map:^id(NSString *text) {
     return  [self isValidSearchText:text] ?
       [UIColor whiteColor] : [UIColor yellowColor];
   }]
   subscribeNext:^(UIColor *color) {
     @strongify (self)
     self.searchText.backgroundColor = color;
   }];

@weakify和@strongify语句是在Extended Objective-C库的宏定义,他们也包含在ReactiveCocoa中。@weakify 宏定义允许你创建一个若饮用的影子变量,@strongify宏定义允许你创建一个前面使用@weakify传递的强引用变量。

Note:如果你对@weakify和@strongify感兴趣,可以进入RACEXTSCope.h中查看其实现。

最后一个提醒,当在Blocks使用实例变量的时候要小心,这样也会导致block捕获一个self的强引用。你可以打开编译警告去告诉你你的代码有这个问题。

AvoidRetainSelf.png

好了,你从理论中幸存出来了,恭喜。现在你变得更加明智,准备移步到有趣的环节:添加一些真实的函数到你的工程里面。

Requesting Access to Twitter

为了在TwitterInstant 应用中去搜索Tweets,你将会用到社交框架(Social Framework)。为了访问Twitter你需要使用Accounts Framework。

在你添加代码之前,你需要到模拟器中输入你的账号:

Simulator Screen Shot 2016年1月16日 下午2.19.19.png

设置好账号之后,然后你只需要在RWSearchFormViewController.m中导入以下文件即可:

1
# import  # import

然后在引入的头文件下面写如下的代码:

1
2
3
4
5
typedef NS_ENUM(NSInteger, RWTwitterInstantError) {
     RWTwitterInstantErrorAccessDenied,
     RWTwitterInstantErrorNoTwitterAccounts,
     RWTwitterInstantErrorInvalidResponse}; 
static  NSString *  const  RWTwitterInstantDomain = @ "TwitterInstant" ;

你将会使用这些简单地鉴定错误。然后在interface和end之间声明两个属性:

1
2
@property  (strong, nonatomic) ACAccountStore *accountStore;
@property  (strong, nonatomic) ACAccountType *twitterAccountType;


ACAccountsStore类提供访问你当前设备有的social账号,ACAccountType类代表指定类型的账户。

然后在viewDidLoad里面加入以下代码:

1
2
self.accountStore = [[ACAccountStore alloc] init];
self.twitterAccountType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];

这些代码创建了账户存储和Twitter账号标示。在.m中添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (RACSignal *)requestAccessToTwitterSignal {
 
   // 1 - define an error
   NSError *accessError = [NSError errorWithDomain:RWTwitterInstantDomain
                                              code:RWTwitterInstantErrorAccessDenied
                                          userInfo:nil];
   // 2 - create the signal
   @weakify (self)
   return  [RACSignal createSignal:^RACDisposable *(id subscriber) {
     // 3 - request access to twitter
     @strongify (self)
     [self.accountStore
        requestAccessToAccountsWithType:self.twitterAccountType
          options:nil
       completion:^(BOOL granted, NSError *error) {
           // 4 - handle the response
           if  (!granted) {
             [subscriber sendError:accessError];
           else  {
             [subscriber sendNext:nil];
             [subscriber sendCompleted];
           }
         }];
     return  nil;
   }];
}

这个方法的作用是:

1、定义了如果用户拒绝访问的错误

2、根据第一个入门教程,类方法createSignal返回了一个RACSignal的实例。

3、通过账户存储请求访问Twitter。在这一点上,用户将看到一个提示,要求他们给予这个程序访问Twitter账户的弹框。

4、当用户同意或者拒绝访问,信号事件就会触发。如果用户同意访问,next事件将会紧随而来,然后是completed发送,如果用户拒绝访问,error事件会触发。

如果你回想其第一个入门教程,一个信号可以以三种不同的事件发出:

1、next

2、completed

3、error

超过了signal的生命周期,它将不会发出任何信号事件。

最后,为了充分利用信号,在viewDidLoad后面添加如下代码;

1
2
3
4
[[self requestAccessToTwitterSignal]
   subscribeNext:^(id x) {
     NSLog(@ "Access granted" );  } error:^(NSError *error) {
     NSLog(@ "An error occurred: %@" , error);  }];

如果你运行程序,将会看到一个弹出框:

Simulator Screen Shot 2016年1月16日 下午2.59.49.png

提示是否允许访问权限,如果ok,则打印出来Access granted ,否则将会走error。

Accounts Framework会记住你的决定,因此如果想再次测试,你需要针对模拟机进行:Reset Contents and Settings。

Chaining Signals

一旦用户允许访问Twitter账户,为了执行twitter,程序将会不断监听搜索内容textField的变化.

程序需要等待信号,它请求访问Twitter去发出completed事件,然后订阅textField的信号。不同信号连续的链是一个共有的问题,但是ReactiveCocoa处理起来非常优雅。

用下面的代码替换当前在viewDidLoad后面的管道:

1
2
3
4
5
6
7
8
9
10
[[[self requestAccessToTwitterSignal]
   then:^RACSignal *{
     @strongify (self)
     return  self.searchText.rac_textSignal;
   }]
   subscribeNext:^(id x) {
     NSLog(@ "%@" , x);
   } error:^(NSError *error) {
     NSLog(@ "An error occurred: %@" , error);
   }];

then方法会一直等待,知道completed事件发出,然后订阅者通过自己的block参数返回,这有效地将控制从一个信号传递给下一个。

Note:上面已经写过了@weakly(self);所以这里就不用再写了。

then方法传递error事件。因此最后的subscribeNext:error: block还接收初始的访问请求错误。

当你运行的时候,然后允许访问,你应该可以在控制台看到打印出来的你输入的东西。

然后,添加filter操作到管道去移除无效的搜索字符串。在这个实例中,他们是不到三个字符的string:

1
2
3
4
5
6
7
8
9
10
[[[[self requestAccessToTwitterSignal]
   then:^RACSignal *{
     @strongify (self)
     return  self.searchText.rac_textSignal;  }]
   filter:^BOOL(NSString *text) {
     @strongify (self)
     return  [self isValidSearchText:text];  }]
   subscribeNext:^(id x) {
     NSLog(@ "%@" , x);  } error:^(NSError *error) {
     NSLog(@ "An error occurred: %@" , error);  }];

运行就可以在控制台看到只有三个以上的才能输出。

图解一下上边的管道:

PipelineWithThen.png

程序管道从requestAccessToTwitterSignal信号开始,然后转换到tac_textSignal。同事next事件通过filter,最后到达订阅block.你也可以看到任何通过第一步的error事件。

现在你有一个发出搜索text的信号,它可以用来搜索Twitter了。很有趣吧。

Searching Twitter

Social Framework是一个访问Twitter 搜索API的选项。然而,它并无法响应搜索,下一步就是给信号包括API请求方法。在当前的控制器中,添加如下方法:

1
2
3
4
5
- (SLRequest *)requestforTwitterSearchWithText:(NSString *)text { 
      NSURL *url = [NSURL URLWithString:@ "https://api.twitter.com/1.1/search/tweets.json" ];      NSDictionary *params = @{@ "q"  : text};   
      SLRequest *request =  [SLRequest requestForServiceType:SLServiceTypeTwitter                                           requestMethod:SLRequestMethodGET  URL:url parameters:params        ];  
      return  request;
  }

这创建了一个请求:搜索Twitter(V.1.1REST API)。这个是调用Twitter的api。

下一步就是创建一个基于request的信号量。添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
- (RACSignal *)signalForSearchWithText:(NSString *)text {
  
   // 1 - define the errors
   NSError *noAccountsError = [NSError errorWithDomain:RWTwitterInstantDomain
                                                  code:RWTwitterInstantErrorNoTwitterAccounts
                                              userInfo:nil]; 
   NSError *invalidResponseError = [NSError errorWithDomain:RWTwitterInstantDomain
                                                       code:RWTwitterInstantErrorInvalidResponse
                                                   userInfo:nil]; 
   // 2 - create the signal block
   @weakify (self)
   return  [RACSignal createSignal:^RACDisposable *(id subscriber) {
     @strongify (self); 
     // 3 - create the request
     SLRequest *request = [self requestforTwitterSearchWithText:text]; 
     // 4 - supply a twitter account
     NSArray *twitterAccounts = [self.accountStore
       accountsWithAccountType:self.twitterAccountType];     if  (twitterAccounts.count ==  0 ) {
       [subscriber sendError:noAccountsError];    }  else  {
       [request setAccount:[twitterAccounts lastObject]]; 
       // 5 - perform the request
       [request performRequestWithHandler: ^(NSData *responseData,                                          NSHTTPURLResponse *urlResponse, NSError *error) {
         if  (urlResponse.statusCode ==  200 ) {
  
           // 6 - on success, parse the response
           NSDictionary *timelineData =
              [NSJSONSerialization JSONObjectWithData:responseData
                                              options:NSJSONReadingAllowFragments
                                                error:nil];          [subscriber sendNext:timelineData];          [subscriber sendCompleted];        }
         else  {
           // 7 - send an error on failure
           [subscriber sendError:invalidResponseError];        }
       }];    }
  
     return  nil;  }];}

然后在viewDidLoad方法中进一步添加信号量:

1
2
3
4
5
6
7
8
9
10
11
12
13
[[[[[self requestAccessToTwitterSignal]
   then:^RACSignal *{
     @strongify (self)
     return  self.searchText.rac_textSignal;  }]
   filter:^BOOL(NSString *text) {
     @strongify (self)
     return  [self isValidSearchText:text];  }]
   flattenMap:^RACStream *(NSString *text) {
     @strongify (self)
     return  [self signalForSearchWithText:text];  }]
   subscribeNext:^(id x) {
     NSLog(@ "%@" , x);  } error:^(NSError *error) {
     NSLog(@ "An error occurred: %@" , error);  }];

运行:

即可在控制台里面打印出来筛选的数据。

Threading

我很确信你这会亟待把JSON数据放到UI里面,但是在放到UI里面之前你需要做最后一件事:找到他是什么,你需要做一些探索!

添加一个端点到subscribeNext:error:那个步,然后我们会看到Xcode左侧的Thread,我们发现如果想加载图片的话必须在主线程里面,但是他不在主线程中,所以我们就可以做如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[[[[[[self requestAccessToTwitterSignal]
   then:^RACSignal *{
     @strongify (self)
     return  self.searchText.rac_textSignal;
   }]
   filter:^BOOL(NSString *text) {
     @strongify (self)
     return  [self isValidSearchText:text];
   }]
   flattenMap:^RACStream *(NSString *text) {
     @strongify (self)
     return  [self signalForSearchWithText:text];
   }]
   deliverOn:[RACScheduler mainThreadScheduler]]
   subscribeNext:^(id x) {
     NSLog(@ "%@" , x);
   } error:^(NSError *error) {
     NSLog(@ "An error occurred: %@" , error);
   }];

这样就会在主线程中运行。也就是更新了管道:添加了deliverOn:操作。

然后再次运行我们就会发现他是在主线程上执行了。这样你就可以更新UI了。

Updating the UI

这里用到了另一个库:LinqToObjectiveC。安装方式就不说了和ReactiveCocoa一样

我们在RWSearchFormViewController中导入:

1
2
# import  "RWTweet.h"
# import  "NSArray+LinqExtensions.h"

然后在输出json数据的地方修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[[[[[[self requestAccessToTwitterSignal]
   then:^RACSignal *{
     @strongify (self)
     return  self.searchText.rac_textSignal;  }]
   filter:^BOOL(NSString *text) {
     @strongify (self)
     return  [self isValidSearchText:text];  }]
   flattenMap:^RACStream *(NSString *text) {
     @strongify (self)
     return  [self signalForSearchWithText:text];  }]
   deliverOn:[RACScheduler mainThreadScheduler]]
   subscribeNext:^(NSDictionary *jsonSearchResult) {
     NSArray *statuses = jsonSearchResult[@ "statuses" ];    NSArray *tweets = [statuses linq_select:^id(id tweet) {
       return  [RWTweet tweetWithStatus:tweet];    }];    [self.resultsViewController displayTweets:tweets];  } error:^(NSError *error) {
     NSLog(@ "An error occurred: %@" , error);  }];

运行:

就可以看到右侧的详情页面加载到数据了。刚引入的类库其实就是将json数据转换成了model.加载数据的效果如下:

Simulator Screen Shot 2016年1月16日 下午4.28.10.png

Asynchronous Loading of Images

现在内容都加载出来了,就差图片了。在RWSearchResultsViewController.m中添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl {
  
   RACScheduler *scheduler = [RACScheduler
                          schedulerWithPriority:RACSchedulerPriorityBackground];
  
   return  [[RACSignal createSignal:^RACDisposable *(id subscriber) {
     NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
     UIImage *image = [UIImage imageWithData:data];
     [subscriber sendNext:image];
     [subscriber sendCompleted];
     return  nil;
   }] subscribeOn:scheduler];
  
}

这会你一ing该就会很熟悉这种模式了。然后在tableview:cellForRowAtIndex:方法里面添加:

1
2
3
4
5
6
7
cell.twitterAvatarView.image = nil;
  
[[[self signalForLoadingImage:tweet.profileImageUrl]
   deliverOn:[RACScheduler mainThreadScheduler]]
   subscribeNext:^(UIImage *image) {
    cell.twitterAvatarView.image = image;
   }];

再次运行就可以出来效果了:

Simulator Screen Shot 2016年1月16日 下午5.07.35.png

Throttling(限流)

你可能注意到这个问题:每次输入一个字符串都会立即执行然后导致刷新太快 ,导致每秒会显示几次搜索结果。这不是理想的状态。

一个好的解决方式就是如果搜索内容不变之后的时间间隔后在搜索比如500毫秒。

而ReactiveCocoa是这个工作变的如此简单。

打开RWSearchFormViewController.m然后更新管道,调整如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[[[[[[[self requestAccessToTwitterSignal]
   then:^RACSignal *{
     @strongify (self)
     return  self.searchText.rac_textSignal;
   }]
   filter:^BOOL(NSString *text) {
     @strongify (self)
     return  [self isValidSearchText:text];
   }]
   throttle: 0.5 ]
   flattenMap:^RACStream *(NSString *text) {
     @strongify (self)
     return  [self signalForSearchWithText:text];
   }]
   deliverOn:[RACScheduler mainThreadScheduler]]
   subscribeNext:^(NSDictionary *jsonSearchResult) {
     NSArray *statuses = jsonSearchResult[@ "statuses" ];
     NSArray *tweets = [statuses linq_select:^id(id tweet) {
       return  [RWTweet tweetWithStatus:tweet];
     }];
     [self.resultsViewController displayTweets:tweets];
   } error:^(NSError *error) {
     NSLog(@ "An error occurred: %@" , error);
   }];

你会发现这样就可以了。throttle操作只是发送一个操作,这个操作在时间到之后继续进行。

Wrap Up

CompletePipeline.png

现在我们知道ReactiveCocoa是多么的优雅。

最后一点:ReactiveCocoa使使用MVVM成为可能。

附:最终代码

Swift响应式编程

响应式编程掀起了Swift的革命,那么它的背后是什么呢?Rx的神秘面具下又是什么呢?最近ReactiveX / RxSwift项目参与者Junior Bontognali在他的博客上发表了一篇文章The Reactive Revolution of Swift,介绍了自己对响应式编程原理的理解,帮助我们掀开了响应式编程和Rx的神秘面纱,本文便是由该文翻译整理而来。

我曾讲过RxSwift这个令人惊叹的抽象概念/框架,以及它可以怎样帮我们简化处理异步编程。这里我讲一下响应式编程(Reactive Programming)是如何将异步编程推到一个全新高度的。

异步编程真的很难

大多数有关响应式编程的演讲和文章都是在展示Reactive框架如何好如何惊人,给出一些在非常复杂的情况下,只需几行代码就可以搞定的例子。例子么?我这里有一段基于RxSwift的聊天程序的代码

socket.rx_event
    .filter({ $0.event == "newMessage" && $0.items?.count > 0})
    .map({ Array($0.items!) })
    .subscribeNext { data in
        let username = data[0]["username"] as? String ?? "unknown"
        let text = data[0]["message"] as? String ?? "invalid text"
        let message = JSQMessage(senderId: username, displayName: username, text: text)
        self.messages += [message]
        self.finishSendingMessage()
    }.addDisposableTo(disposeBag)

这段代码展示了socket事件是如何被过滤和处理,以显示某个特定的用户在聊天中发送的消息的。
经典的方法可能会以类似下面的代码作为结束:

dispatch_async(dispatch_get_global_queue()) {

         let socketData = self.loadDataFromSocket()
         let data = self.parseData(data)

         dispatch_async(dispatch_get_main_queue()) { 
            let username = data[0]["username"] as? String ?? "unknown"
            let text = data[0]["message"] as? String ?? "invalid text"
            let message = JSQMessage(senderId: username, displayName: username, text: text)
            self.messages += [message]
            self.finishSendingMessage()
         }
    }

这就是所谓的“回调地狱”,代码难以阅读和维护。但是除了真的很难阅读外,回调为什么这么不好呢?

同步变得痛苦

同步编程不仅仅是在一个单独的线程中运行任务或者执行计算,在某些时候,我们需要在多个线程中同步数值,而最常见的解决方法就是添加锁。一旦锁被引入,代码的复杂性就至少提高了一个数量级,而且又引入了一个新问题:不可预测性。

在计算方面,代码现在变得不可预测。当一个单一线程被调度时,以及假如我们访问一个预期的单一(已上锁的)属性值时,没有办法可以确定地知道我们是否会丢掉值或者正在处理和之前相同的值。

那么异步编程的真正困难是什么呢?答案是同步。听起来很有意思,但这是真的。

开始使用响应式方法

响应式编程不是我们很多人所想的那样是一个年轻的概念,这一点很重要。它的起源可以追溯到1969年,计算机科学的传奇人物Alan Kay在犹他大学的那篇名为“The Reactive Engine”的博士学位论文。但我不是在这里给大家上历史课的,考虑到有些概念听起来很好,实验起来却没那么好用,所以让我们看看响应式编程在处理同步代码时所体现出的价值。

Rx的世界中,最根本的部分是观察者模式(Observer pattern)和迭代器模式(Iterator pattern)的结合,两者都是众所周知的模式,而且在编写软件时被广泛用于处理一些特定的行为。观察者模式和迭代器模式,这两者是相互作用的,在两种情况下的计算的都是从生产者中取出的值。对于迭代器模式,只要他们是可用的,我们就可以获取值,对于观察者模式,我们是在生产者给所有具体的观察者发出通知后,从中获取数据去处理值。获取绝对是一个很好的既定具体解决方案,只要一切都是在同一线程中处理的,它就能很好地起作用。当数据被获取出来,进行一步的处理时会发生什么呢?好了,此时我们添加的锁开始发挥作用了,事情很快就难办了。

设想情景

我们设想一下,有一个非常好的应用,已经准备好了在几天内发布,但是在最后的测试中,在某些情况下,一些死锁和征用条件产生了,而应用的崩溃是随机的,只有足够的数据才能够去确定这个问题。时间是有限的,问题在可能出现的地方比预期的还要发生得更快,但是解决方案或许不能那么简单和快速地开发和部署出来。首先要记住的是,一旦采用了锁定策略,那么整个代码的运行速度都会减慢,同时异步编程的不可预测性也会影响到一般应用程序的性能。也有可能会有数量级的增长放缓,我们可以得到的结论是:这是不可接受的。

什么是响应式

响应式是一种设计,获取数据是一个绝对的东西,可能很难在很短的一段时间内调整,所以一个解决方案是翻转该行为,即为什么不能让资源自己把值推送给一个用户/消费者,而是我们去从一个资源获取呢?这就是Rx具体的内容,即推送数据给被生产者订阅了的实例。我们在某些地方产生数据,然后根据需求将数据推送给用户并进行处理。

观察者 + 迭代器 + 推送 = 可观测的(Observable)实体

Rx背后的数学很简单,两个非常确定的实体,结合不同的交互模型,成为了一个可观测的实体的根基。这就是革命发生的地方,结合旧的,既定的概念,以一定的方式创建和模拟一个强大的抽象概念,来帮助处理异步编程,而不用去冒着项目在最后一周挂掉的危险。

这个数学公式的结果是一个叫做可观测的实体,这个对象负责处理原始数据,并在必要时将值推送给用户。用户可以扮演多种角色,它们可以成为别的连锁的可观测实体、操作者或者仅仅是回调。Rx是非常基本的,但也是非常强大的抽象概念。

声明

使用可观测实体的方法要求初始化可观测实体时声明逻辑,这意味着我们的代码变得更加紧凑,而且通常被限制在view Controller的初始化方法中。在这个的RxChat的例子中,逻辑的大部分都是在viewDidLoad的函数中声明的:

 override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        self.setup()

        user
            .distinctUntilChanged()
            .subscribeNext() { username in
                self.senderId = username
                self.senderDisplayName = username
                self.socket.emit("addUser", withItems:[username])
            }.addDisposableTo(disposeBag)

        socket.rx_event
            .filter({ $0.event == "connect"})
            .map({ _ in return () })
            .subscribeNext { _ in
                self.title = "RxChat (Connected)"
            }.addDisposableTo(disposeBag)

        // [...]
    }

这种方法也被称为声明式编程(Declarative Programming),可以帮助我们有效减少潜在bug的数量,也使得在使用MVVM或者类似的模式时所写出的代码让人眼前一亮。

现在做什么呢?

一般来说,学习Rx或响应式编程就像学习一门新的语言(不是指编程语言),步骤是非常相似的,我们在最初的阶段学习基本语法和常用句子,然后学习规则和语义,最后我们能够说出某些甚至最难的话题时,我们就掌握了这门语言。

为了持续学习,我建议读者阅读以下资料:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值