这周在学习RAC,找到一篇很好的文章,(摘录下来,对部份内容进行必定的翻译,感受翻译要比简单的读,更须要字斟句酌,更容易留下较深的印象,加深理解,翻译出来,也能够帮助到别人)。本来计划是部分翻译,最后没有掌握好,仍是所有翻译了,一共花费了将近十个小时,以前对这篇文章的研究也花费了将近十个小时,这样效率有点低,下一步,要看看怎么提升一下这个效率。html
这篇文章跟着走一遍,对于RAC基本就能有必定的印象了,而后再针对每一个细节来进行学习,就能较快掌握住RAC了,不然,只看官网,官网的文章写得感受并非很清楚。react
原帖位置:https://www.raywenderlich.com/2493-reactivecocoa-tutorial-the-definitive-introduction-part-1-2。ios
Get to grips with ReactiveCocoa in this 2-part tutorial series. Put the paradigms to one-side, and understand the practical value with work-through examples.git
在这两部分指导系统掌握 ReactiveCocoa。先把范式放在一边,先经过对这些例子的实践来理解实际的意思。github
As an iOS developer, nearly every line of code you write is in reaction to some event; a button tap, a received network message, a property change (via Key Value Observing) or a change in user’s location via CoreLocation are all good examples. However, these events are all encoded in different ways; as actions, delegates, KVO, callbacks and others. ReactiveCocoa defines a standard interface for events, so they can be more easily chained, filtered and composed using a basic set of tools.objective-c
做为一个iOS开发者,几乎你编写的每一行代码都是一个对某种事件的响应;一个按钮的点击,一个网络消息的接收,一个属性的改变(经过Key Value Observing)或者经过 CoreLocation 的用户位置的改变都是很好的例子。而后,这些事件被用不一样的方式进行的编码;被当作 action, delegates, KVO, callback 和其余。ReactiveCocoa 定义了一个标准的事件界面,这样他们能够被更容易地链化,过滤和使用一个基本的工具集来编写。express
Sound confusing? Intriguing? … Mind blowing? Then read on :]编程
听上去有些困惑?有趣? ... 使人兴奋?来继续阅读吧 :]浏览器
ReactiveCocoa combines a couple of programming styles:网络
ReactiveCocoa 由一组编程风格组成:
- Functional Programming which makes use of higher order functions, i.e. functions which take other functions as their arguments
函数式编程 这使用了不少高阶函数,例如函数使用其它函数做为参数
- Reactive Programming which focuses of data-flows and change propagation
响应式编程 这聚焦于数据流和改变的传播
For this reason, you might hear ReactiveCocoa described as a Functional Reactive Programming (or FRP) framework.
由于这个缘由,你可能听到过把ReactiveCocoa描述成一个函数响应式框架。
Rest assured, that is as academic as this tutorial is going to get! Programming paradigms are a fascinating subject, but the rest of this ReactiveCocoa tutorials focuses solely on the practical value, with work-through examples instead of academic theories.
请放心,这就像本教程的学术内容同样! 编程范式是一个引人入胜的主题,但ReactiveCocoa教程的其他部分仅关注实用价值,并经过实例来代替学术理论。
Throughout this ReactiveCocoa tutorial, you’ll be introducing reactive programming to a very simple example application, the ReactivePlayground. Download the starter project, then build and run to verify you have everything set up correctly.
经过这个指导,你将引入响应式编程到一个很是简单的样例程序中,就是这个响应式游乐场。下载这个starter project,而后编译而且运行它去校验你已经正确安装了全部的事情。
ReactivePlayground is a very simple app that presents a sign-in screen to the user. Supply the correct credentials, which are, somewhat imaginatively, user for the username, and password for the password, and you’ll be greeted by a picture of a lovely little kitten.
ReactivePlayground 是一个很是简单的app表明了一个sign-in屏幕给用户。提供正确的凭证,有点想象力,用户名是user, 密码是 password, 而后你将会看到一只可爱的小猫的欢迎页。
Awww! How cute!
喔!多么可爱!
Right now it’s a good point to spend a little time looking through the code of this starter project. It is quite simple, so it shouldn’t take long.
如今是时候花费一些时间去看一下这个项目的代码。它很是简单,因此这不用花很长时间。
Open RWViewController.m and take a look around. How quickly can you identify the condition that results in the enabling of the Sign In button? What are the rules for showing / hiding the
signInFailure
label? In this relatively simple example, it might take only a minute or two to answer these questions. For a more complex example, you should be able to see how this same type of analysis might take quite a bit longer.
打开RWViewController.m大概看一下。 您能多快识别出产生启用Sign In按钮的条件? 显示/隐藏signInFailure标签的规则是什么? 在这个相对简单的例子中,回答这些问题可能只须要一两分钟。 对于更复杂的示例,您应该可以看到相同类型的分析可能须要更长的时间。
With the use of ReactiveCocoa, the underlying intent of the application will become a lot clearer. It’s time to get started!
随着ReactiveCocoa的使用,这个程序的潜在乎图会更加清楚。让咱们开始吧!
The easiest way to add the ReactiveCocoa framework to your project is via CocoaPods. If you’ve never used CocoaPods before it might make sense to follow the Introduction To CocoaPods tutorial on this site, or at the very least run through the initial steps of that tutorial so you can install the prerequisites.
最容易的增长ReactiveCocoa框架到你的项目中的方法是经过CocoaPods。若是你没有使用过它,那么遵循着Introduction To CocoaPods的指导是有意义的,或者至少运行一下那个指导的初始步骤,因此你就能够安装依赖了。
Note: If for some reason you don’t want to use CocoaPods you can still use ReactiveCocoa, just follow the Importing ReactiveCocoa steps in the documentation on GitHub.
注意:若是由于什么缘由,你不想使用CocoaPods, 你仍然可使用ReactiveCocoa, 只要跟着在Github上的Importing ReactiveCocoa的文档的步骤。
If you still have the ReactivePlayground project open in Xcode, then close it now. CocoaPods will create an Xcode workspace, which you’ll want to use instead of the original project file.
若是你的ReactivePlayground项目是打开的,那么如今关上它。CocoaPods将会建立一个Xcode工做空间,你可使用那个来替换本来的项目文件。
Open Terminal. Navigate to the folder where your project is located and type the following:
打开终端。切换到项目目录,输入如下命令:
touch Podfile open -e Podfile
This creates an empty file called Podfile and opens it with TextEdit. Copy and paste the following lines into the TextEdit window:
这将建立一个空的名字是Podfile的文件,用文本编辑器打开它。复制而且贴入下面的代码:
platform :ios, '7.0' pod 'ReactiveCocoa', '2.1.8'
This sets the platform to iOS, the minimum SDK version to 7.0, and adds the ReactiveCocoa framework as a dependency.
这个设置了iOS的平台,最低SDK版本是7.0,而且增长了ReactiveCocoa框架做为依赖。
Once you’ve saved this file, go back to the Terminal window and issue the following command:
一旦你保存了这个文件,回到终端,输入下面的命令:
pod install
You should see an output similar to the following:
你将会看到相似如下的输出:
Analyzing dependencies Downloading dependencies Installing ReactiveCocoa (2.1.8) Generating Pods project Integrating client project [!] From now on use `RWReactivePlayground.xcworkspace`.
This indicates that the ReactiveCocoa framework has been downloaded, and CocoaPods has created an Xcode workspace to integrate the framework into your existing application.
这意味着ReactiveCocoa框架已经被下载,而且CocoaPods建立了一个Xcode工做空间,集成了框架到你的程序中。
Open up the newly generated workspace, RWReactivePlayground.xcworkspace, and look at the structure CocoaPods created inside the Project Navigator:
打开新生成的工做空间,RWReactivePlayground.xcworkspace,看一下 CocoaPods 在项目浏览器中建立的结构:
You should see that CocoaPods created a new workspace and added the original project, RWReactivePlayground, together with a Pods project that includes ReactiveCocoa. CocoaPods really does make managing dependencies a breeze!
你应该看到 CocoaPods 建立了一个新的工做空间,而且增长了原始的项目,RWReactivePlayground,和一个包含着ReactiveCocoa的 Pods 项目在一块儿。CocoaPods 确实使管理依赖变得垂手可得。
You’ll notice this project’s name is
ReactivePlayground
, so that must mean it’s time to play …
你将会注意到这个项目的名字是 ReactivePlayground, 因此是时候来玩了 ...
As mentioned in the introduction, ReactiveCocoa provides a standard interface for handling the disparate stream of events that occur within your application. In ReactiveCocoa terminology these are called signals, and are represented by the
RACSignal
class.
就像介绍里面提到,ReactiveCocoa提供了一个标准的界面,来处理不一样的产生自你的应用中的事件流。在ReactiveCocoa的术语中,它们被称为signal(信号,后文都称为signal),被RACSignal类所表明。
Open the initial view controller for this app, RWViewController.m, and import the ReactiveCocoa header by adding the following to the top of the file:
打开这个应用的初始的view controller,RWViewController.m, 引入ReactiveCocoa头文件。
#import <ReactiveCocoa/ReactiveCocoa.h>
You aren’t going to replace any of the existing code just yet, for now you’re just going to play around a bit. Add the following code to the end of the
viewDidLoad
method:
你还不能替代现有的任何代码,你能够先运行一下。在viewDidLoad方法
底部加入下面的代码:
[self.usernameTextField.rac_textSignal subscribeNext:^(id x) { NSLog(@"%@", x); }];
Build and run the application and type some text into the username text field. Keep an eye on the console and look for an output similar to the following:
编译而且运行这个程序,在username文本域输入一些字符。留心控制台,会看到相似下面的输出:
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?
You can see that each time you change the text within the text field, the code within the block executes. No target-action, no delegates — just signals and blocks. That’s pretty exciting!
你能够看到每一次你在这个文本域修改了文本,这个 block 中的代码会运行。没有target-action, 没有delegates ----只有signal和 block。是否是很使人兴奋!
ReactiveCocoa signals (represented by
RACSignal
) send a stream of events to their subscribers. There are three types of events to know: next, error and completed. A signal may send any number of next events before it terminates after an error, or it completes. In this part of the tutorial you’ll focus on the next event. Be sure to read part two when it’s available to learn about error and completed events.
ReactiveCocoa signals(被 RACSignal 所表明
)发送了一个事件流给它们的订阅者。有三种类型的时间须要了解:next,error 和 completed。一个signal在它在一个error或者completed结束以前能够发送任意数量的next事件。在这部分的指导中,你将会聚焦于next事件。当有条件去学习error和completed事件时,要保证去阅读part two。
RACSignal
has a number of methods you can use to subscribe to these different event types. Each method takes one or more blocks, with the logic in your block executing when an event occurs. In this case, you can see that thesubscribeNext:
method was used to supply a block that executes on each next event.
RACSignal有不少方法,你能够用来去订阅这些不一样的事件类型。每个方法接收一个或多个block,当事件发生时,block中的逻辑会被执行。在这种状况下,您能够看到subscribeNext:方法用于提供在每一个next事件上执行的块。
The ReactiveCocoa framework uses categories to add signals to many of the standard UIKit controls so you can add subscriptions to their events, which is where the
rac_textSignal
property on the text field came from.
ReactiveCocoa框架使用了categories去给不少标准的 UIKit 组件增长signals,这样你能够给它们增长订阅,这就是这个文本域的rac_textSignal域的来源。
But enough with the theory, it’s time to start making ReactiveCocoa do some work for you!
可是理论足够(这里为何要用“可是”),是时候开始让ReactiveCocoa为你作一些工做了!
ReactiveCocoa has a large range of operators you can use to manipulate streams of events. For example, assume you’re only interested in a username if it’s more than three characters long. You can achieve this by using the
filter
operator. Update the code you added previously inviewDidLoad
to the following:
ReactiveCocoa有不少的操做符,你能够用来操纵事件流。举例来讲,假设你只对3个字符以上的username感兴趣。你能够用filter 操做符来获得这个。更新以前你加入到viewDidLoad的代码:
[[self.usernameTextField.rac_textSignal filter:^BOOL(id value) { NSString *text = value; return text.length > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
If you build and run, then type some text into the text field, you should find that it only starts logging when the text field length is greater than three characters:
若是您构建并运行,而后在文本字段中键入一些文本,您会发现它只在文本字段长度大于三个字符时才开始记录:
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?
What you’ve created here is a very simple pipeline. It is the very essence of Reactive Programming, where you express your application’s functionality in terms of data flows.
你在这里建立的是一个很是简单的管道。 这是Reactive Programming的本质,您能够根据数据流表达应用程序的功能。
It can help to picture these flows graphically:
用图片来表示这个流是颇有帮助的:
In the above diagram you can see that the
rac_textSignal
is the initial source of events. The data flows through afilter
that only allows events to pass if they contain a string with a length that is greater than three. The final step in the pipeline issubscribeNext:
where your block logs the event value.
在上面这个图中,你能够看到rac_textSignal是事件的初始来源。数据流经过一个filter,仅仅容许包含字符串长度大于三的事件经过。管道的最后一步是
subscribeNext:
,那里你的 block 输出了事件的值。
At this point it’s worth noting that the output of the
filter
operation is also anRACSignal
. You could arrange the code as follows to show the discrete pipeline steps:
在这点上,值得注意的是,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); }];
Because each operation on an
RACSignal
also returns anRACSignal
it’s termed a fluent interface. This feature allows you to construct pipelines without the need to reference each step using a local variable.
由于在一个RACSignal的每个操做都返回一个RACSignal,因此它被称为
fluent interface。这个特征容许你去构建管道,而不须要使用一个本地变量去指向管道的每一步。
Note: ReactiveCocoa makes heavy use of blocks. If you’re new to blocks, you might want to read Apple’s Blocks Programming Topics. And if, like me, you’re familiar with blocks, but find the syntax a little confusing and hard to remember, you might find the amusingly titled f*****gblocksyntax.com quite useful! (We censored the word to protect the innocent, but the link is fully functional.)
注意:ReactiveCocoa大量地使用了 blocks。若是你对block不了解,你应该去阅读苹果的 Blocks Programming Topics。或者,像我同样,你对blocks比较熟悉,可是发现这个语法有一些困扰,或者难于记忆,你能够去看看 f*****gblocksyntax.com,会颇有帮助。(这个网址带着一些脏字,为了照顾读者,因此这里隐去了几个字母,可是连接是彻底有效的。)
If you updated your code to split it into the various
RACSignal
components, now is the time to revert it back to the fluent syntax:
若是你更新你的代码,把它分解成几个RACSignal组件,如今是时候把它转换成流利的语法:
[[self.usernameTextField.rac_textSignal filter:^BOOL(id value) { NSString *text = value; // implicit cast return text.length > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
The implicit cast from
id
toNSString
, at the indicated location in the code above, is less than elegant. Fortunately, since the value passed to this block is always going to be anNSString
, you can change the parameter type itself. Update your code as follows:
这个从id到 NSString的隐含转换,在上面代码的带有注释的哪一行,是不优雅的。幸运的是,由于传递给这个block的值永远应该是一个NSString,因此你能够修改入参类型,就像下面这样:
[[self.usernameTextField.rac_textSignal filter:^BOOL(NSString *text) { return text.length > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
Build and run to confirm this works just as it did previously.
构建而且运行去确认它工做起来和以前是同样的。
So far this tutorial has described the different event types, but hasn’t detailed the structure of these events. What’s interesting is that an event can contain absolutely anything!
到这里,这份指导已经描述了不一样的事件类型,可是尚未揭示这些事件的结构细节。有趣的是,一个事件能够包含任何东西!
As an illustration of this point, you’re going to add another operation to the pipeline. Update the code you added to
viewDidLoad
as follows:
做为对这点的一个阐述,你能够给管道增长另一个操做。更新你以前增长的代码:
[[[self.usernameTextField.rac_textSignal map:^id(NSString *text) { return @(text.length); }] filter:^BOOL(NSNumber *length) { return [length integerValue] > 3; }] subscribeNext:^(id x) { NSLog(@"%@", x); }];
If you build and run you’ll find the app now logs the length of the text instead of the contents:
若是你构建而且运行它,你回发现如今程序打印的是文本的长度,而再也不是它的内容了:
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
The newly added map operation transforms the event data using the supplied block. For each next event it receives, it runs the given block and emits the return value as a next event. In the code above, the map takes the
NSString
input and takes its length, which results in anNSNumber
being returned.
新增长的map操做使用提供的block转换了事件的数据。对于每个它接收到的next事件,它运行给定的block,而且把返回值做为 next事件发射出去。在上面的代码中,map接收了NSString输入,而且获取了它的长度,用一个NSNumber返回出去。
For a stunning graphic depiction of how this works, take a look at this image:
有关其工做原理的精美图像描述,请查看此图像:
As you can see, all of the steps that follow the
map
operation now receiveNSNumber
instances. You can use themap
operation to transform the received data into anything you like, as long as it’s an object.
就像你能够看到的,全部跟着map的步骤如今都接收到一个NSNumber实例
。你可使用map操做来转换接收到的数据成你想要的任何东西,只要它是一个对象。
Note: In the above example the
text.length
property returns anNSUInteger
, which is a primitive type. In order to use it as the contents of an event, it must be boxed. Fortunately the Objective-C literal syntax provides and option to do this in a rather concise manner –@(text.length)
.
注意:在上面的例子中,这个text.length返回一个NSUInteger, 这是一个原始类型。为了在一个事件的内容中使用它,它必须被包裹。幸运的是,
Objective-C literal syntax提供了一个很是简洁的方式去处理这个 -- @(text.length)。(这里好像是写错了“
provides and option”
彷佛不合语法)
That’s enough playing! It’s time to update the ReactivePlayground app to use the concepts you’ve learned so far. You may remove all of the code you’ve added since you started this tutorial.
这就足够了! 是时候更新ReactivePlayground应用程序以使用您迄今为止学到的概念。 您能够删除自本教程开始以来添加的全部代码。
The first thing you need to do is create a couple of signals that indicate whether the username and password text fields are valid. Add the following to the end of
viewDidLoad
in RWViewController.m:
你须要作的第一件事情是建立一对信号,用来指示username和password 两个文本域是否有效。在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]); }];
As you can see, the above code applies a
map
transform to therac_textSignal
from each text field. The output is a boolean value boxed as aNSNumber
.
就像你看到的,上面代码使用了一个 map 去给每个文本域转换 rac_textSignal。输出是被一个NSNumber包裹的布尔值。
The next step is to transform these signals so that they provide a nice background color to the text fields. Basically, you subscribe to this signal and use the result to update the text field background color. One viable option is as follows:
下一步是去转换这些信号,使得它们能够给这些文本域提供背景色。基本上,您订阅此信号并使用结果更新文本字段背景颜色。 一个可行的选择以下:
[[validPasswordSignal map:^id(NSNumber *passwordValid) { return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor]; }] subscribeNext:^(UIColor *color) { self.passwordTextField.backgroundColor = color; }]; (Please don’t add this code, there’s a much more elegant solution coming!)
Conceptually you’re assigning the output of this signal to the
backgroundColor
property of the text field. However, the code above is a poor expression of this; it’s all backwards!
从概念上讲,您将此信号的输出分配给文本字段的background的Color属性。 可是,上面的代码对此表达的很差; 一切都倒退了!(不是很好的响应式的表达方式)
Fortunately, ReactiveCocoa has a macro that allows you to express this with grace and elegance. Add the following code directly beneath the two signals you added to
viewDidLoad
:
幸运的是,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]; }];
The
RAC
macro allows you to assign the output of a signal to the property of an object. It takes two arguments, the first is the object that contains the property to set and the second is the property name. Each time the signal emits a next event, the value that passes is assigned to the given property.
这个RAC宏容许你分配一个signal的输出给一个对象的属性。它使用了两个参数,第一个是那个对象,包含着将要设置的属性,第二个是属性名。每一次signal发射一个next事件,被传递的值就被分配给给定的属性。
This is a very elegant solution, don’t you think?
这是一种很是优雅的解决方案,你不以为吗?
One last thing before you build and run. Locate the
updateUIState
method and remove the first two lines:
你构建和运行前,最后一件事情,找到updateUIState方法,移除前面两行:
self.usernameTextField.backgroundColor = self.usernameIsValid ? [UIColor clearColor] : [UIColor yellowColor]; self.passwordTextField.backgroundColor = self.passwordIsValid ? [UIColor clearColor] : [UIColor yellowColor];
That will clean up the non-reactive code.
这将清理非响应式的代码。
Build and run the application. You should find that the text fields look highlighted when invalid, and clear when valid.
构建而且运行程序。你能够发现文本域当无效时会变得高亮,有效时高亮会消失。
Visuals are nice, so here is a way to visualize the current logic. Here you can see two simple pipelines that take the text signals, map them to validity-indicating booleans, and then follow with a second mapping to a
UIColor
which is the part that binds to the background color of the text field.
可视化是很好的,因此这里有一个办法是把这个逻辑可视化。(用图表的方式易于理解。)这里你能够看到两个简单的管道,接收文本signal,把他们map成指示有效的布尔型,而后再map成一个 UIColor,和文本域的
background的color属性绑定起来。
Are you wondering why you created separate
validPasswordSignal
andvalidUsernameSignal
signals, as opposed to a single fluent pipeline for each text field? Patience dear reader, the method behind this madness will become clear shortly!
你是否会有疑问,为何要建立validPasswordSignal和validUsernameSignal两个信号,而不是为建立一个流畅的管道?请有点耐心,这种处理背后的方法立刻就会变得清楚(就会出现了)。
In the current app, the Sign In button only works when both the username and password text fields have valid input. It’s time to do this reactive-style!
在当前的程序中,这个Sign In按钮只在username和password两个文本域都有有效的输入时工做。是时候去把这个作成响应式了。
The current code already has signals that emit a boolean value to indicate if the username and password fields are valid;
validUsernameSignal
andvalidPasswordSignal
. Your task is to combine these two signals to determine when it is okay to enable the button.
当前的代码已经有了signal发射了一个布尔值去指示着username和password域是否有效:validUsernameSignal和validPasswordSignal。你的任务是合并这两个signal去决定何时可让这个按钮开始工做。
At the end of
viewDidLoad
add the following:
在viewDidLoad底部增长:
RACSignal *signUpActiveSignal = [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal] reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) { return @([usernameValid boolValue] && [passwordValid boolValue]); }];
The above code uses the
combineLatest:reduce:
method to combine the latest values emitted byvalidUsernameSignal
andvalidPasswordSignal
into a shiny new signal. Each time either of the two source signals emits a new value, the reduce block executes, and the value it returns is sent as the next value of the combined signal.
上面的代码使用了combineLatest:reduce:
方法去合并由validUsernameSignal和validPasswordSignal发射的最后的值到一个闪亮的新的signal。每一次这两个源signal中的一个发射出一个新值,这个reduce block就执行,而且它返回的值被做为
combined signal的next的值。
Note: The
RACSignal
combine methods can combine any number of signals, and the arguments of the reduce block correspond to each of the source signals. ReactiveCocoa has a cunning little utility class,RACBlockTrampoline
that handles the reduce block’s variable argument list internally. In fact, there are a lot of cunning tricks hidden within the ReactiveCocoa implementation, so it’s well worth pulling back the covers!
注意:RACSignal方法能够合并任意数量的信号,reduce block的参数对应于每一个源signal。 ReactiveCocoa有一个狡猾的小实用程序类RACBlockTrampoline,它在内部处理reduce block的变量参数列表。 事实上,在ReactiveCocoa实现中隐藏了许多小技巧,(最后一句没搞明白)!
Now that you have a suitable signal, add the following to the end of
viewDidLoad
. This will wire it up to the enabled property on the button:
如今你有了一个合适的signal, 在viewDidLoad底部增长底下的代码。这会将其链接到按钮上的enabled属性:
[signUpActiveSignal subscribeNext:^(NSNumber *signupActive) { self.signInButton.enabled = [signupActive boolValue]; }];
Before running this code, it’s time to rip out the old implementation. Remove these two properties from the top of the file:
在运行这个代码前,是时候移除旧的实现了。在文件顶部移除这两个属性:
@property (nonatomic) BOOL passwordIsValid; @property (nonatomic) BOOL usernameIsValid;
From near the top of
viewDidLoad
, remove the following:
在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];
Also remove the
updateUIState
,usernameTextFieldChanged
andpasswordTextFieldChanged
methods. Whew! That’s a lot of non-reactive code you just disposed of! You’ll be thankful you did.
而且移除updateUIState
, usernameTextFieldChanged
和passwordTextFieldChanged
方法。喔!你刚刚移除了好多非响应式的代码!你未来会感谢你所作的。
Finally, make sure to remove the call to
updateUIState
fromviewDidLoad
as well.
最后,确认从viewDidLoad移除掉对updateUIState的调用。
If you build and run, check the Sign In button. It should be enabled because the username and password text fields are valid, as they were before.
若是你构建而且运行了,检查Sign In按钮。若是username和password文本域有效,它将可使用,就像前面同样。
An update to the application logic diagram gives the following:
更新应用的逻辑图:
The above illustrates a couple of important concepts that allow you to perform some pretty powerful tasks with ReactiveCocoa;
上面描绘了两个重要的概念,容许你用ReactiveCocoa去完成一些强大的任务;
- Splitting – signals can have multiple subscribers and serve as the source for more multiple subsequent pipeline steps. In the above diagram, note that the boolean signals that indicate password and username validity are split and used for a couple of different purposes.
- Combining – multiple signals may be combined to create new signals. In this case, two boolean signals were combined. However, you can combine signals that emit any value type.
The result of these changes is the application no longer has private properties that indicate the current valid state of the two text fields. This is one of the key differences you’ll find when you adopt a reactive style — you don’t need to use instance variables to track transient state.
这些改变的结果是应用程序再也不拥有私有属性来指示这两个文本域当前的这有效性。这是一个关键的不一样,你将会发现当你采纳响应式方式,你再也不须要实例变量去跟踪瞬时状态。
The application currently uses the reactive pipelines illustrated above to manage the state of the text fields and button. However, the button press handling still uses actions, so the next step is to replace the remaining application logic in order to make it all reactive!
当前的应用程序使用上面所描述的响应式的管道来管理文本域和按钮的状态。而后,按钮的按下处理仍旧使用了actions, 因此下一步是取代这些剩余的逻辑,来让整个程序都变成响应式。
The Touch Up Inside event on the Sign In button is wired up to the
signInButtonTouched
method inRWViewController.m
via a storyboard action. You’re going to replace this with the reactive equivalent, so you first need to disconnect the current storyboard action.
这个在Sign In按钮上的Touch Up Inside事件被经过storyboard的action绑定在RWViewController.m
的signInButtonTouched方法上。你将使用响应式的等价物来要替代这些,因此你先要断开当前的
storyboard的action。
Open up Main.storyboard, locate the Sign In button, ctrl-click to bring up the outlet / action connections and click the x to remove the connection. If you feel a little lost, the diagram below kindly shows where to find the delete button:
打开Main.storyboard, 找到Sign In按钮,按下ctrl和左键来打开outlet / action链接,而且点击那个x来移除这个链接。若是你没跟上,看下图,能够找到那个delete按钮。
You’ve already seen how the ReactiveCocoa framework adds properties and methods to the standard UIKit controls. So far you’ve used
rac_textSignal
, which emits events when the text changes. In order to handle events you need to use another of the methods that ReactiveCocoa adds to UIKit,rac_signalForControlEvents
.
你能够看到了ReactiveCocoa框架如何给标准的UIKit控件增长属性和方法了。到如今为止,你已经使用了rac_textSignal, 它会在文本改变时发射事件。为了处理这些事件,你须要使用
ReactiveCocoa增长给UIKit的另一个方法,rac_signalForControlEvents。
Returning to
RWViewController.m
, add the following to the end ofviewDidLoad
:
返回RWViewController.m, 在viewDidLoad的地步增长下面的代码:
[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) { NSLog(@"button clicked"); }];
The above code creates a signal from the button’s
UIControlEventTouchUpInside
event and adds a subscription to make a log entry every time this event occurs.
上面的代码从按钮的UIControlEventTouchUpInside
建立了一个signal,而且增长了一个订阅,在每一次事件发生时输出一条日志。
Build and run to confirm the message actually logs. Bear in mind that the button will enable only when the username and password are valid, so be sure to type some text into both fields before tapping the button!
构建而且运行代码来确认消息实际上输出了。请记住,只有当用户名和密码有效时才会启用该按钮,所以请务必在点击按钮以前在两个字段中键入一些文本!
You should see messages in the Xcode console similar to the following:
你能够在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
Now that the button has a signal for the touch event, the next step is to wire this up with the sign-in process itself. This presents something of a problem — but that’s good, you don’t mind a problem, right? Open up
RWDummySignInService.h
and take a look at the interface:
注意这个按钮有一个touch事件的signal, 下一步就是把它绑定到sign-in过程当中去。这提出了一个问题 - 但这很好,你不介意一个问题,对吧? 打开RWDummySignInService.h并查看界面:
typedef void (^RWSignInResponse)(BOOL); @interface RWDummySignInService : NSObject - (void)signInWithUsername:(NSString *)username password:(NSString *)password complete:(RWSignInResponse)completeBlock; @end
This service takes a username, a password and a completion block as parameters. The given block is run when the sign-in is successful or when it fails. You could use this interface directly within the
subscribeNext:
block that currently logs the button touch event, but why would you? This is the kind of asynchronous, event-based behavior that ReactiveCocoa eats for breakfast!
此服务将用户名,密码和完成块做为参数。 登陆成功或失败时运行给定的块。 您能够直接在当前记录按钮触摸事件的subscribeNext:块中使用此接口,但为何会这样?
Note: A dummy service is being used in this tutorial for simplicity, so that you don’t have any dependencies on external APIs. However, you’ve now run up against a very real problem, how do you use APIs not expressed in terms of signals?
注意:为简单起见,本教程中使用了虚拟服务,所以您对外部API没有任何依赖性。 可是,您如今遇到了一个很是现实的问题,您如何使用未表达信号的API?
Fortunately, it’s rather easy to adapt existing asynchronous APIs to be expressed as a signal. First, remove the current
signInButtonTouched:
method from the RWViewController.m. You don’t need this logic as it will be replaced with a reactive equivalent.
幸运的是,将已经存在的异步API调整表达为一个signal是一件很容易的事情。首先,从RWViewController.m文件中移除当前的signInButtonTouched:方法。你再也不须要这个逻辑,由于它将被一个响应式的等价物所替代。
Stay in RWViewController.m and add the following method:
留在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; }]; }
The above method creates a signal that signs in with the current username and password. Now for a breakdown of its component parts.
上面的方法建立了一个signal,发射于当前的用户名和密码。如今咱们分解它的组成部分。
The above code uses the
createSignal:
method onRACSignal
for signal creation. The block that describes this signal is a single argument, and is passed to this method. When this signal has a subscriber, the code within this block executes.
上面的代码使用了RACSignal的createSignal:,为了来建立signal。描述了这个signal的block是一个参数,传递给这个方法。当这个signal有一个订阅者,这个block中的代码将会被执行。block这里所指的是这一段:
^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,返回值是RACDisposable *,参数是id<RACSubscriber> subscriber,实现了RACSubscriber协议的subscriber对象。这个地方须要反复理解block的语法和做用,不然很容易不理解。把block当作一个匿名函数来理解,这里体现的就是函数式编程,传递进去的参数再也不是一个变量,而是一个函数,接受这个参数的方法,再也不是对这个变量进行赋值、传递,而是对这个函数进行调用。
The block is passed a single
subscriber
instance that adopts theRACSubscriber
protocol, which has methods you invoke in order to emit events; you may also send any number of next events, terminated with either an error or complete event. In this case, it sends a single next event to indicate whether the sign-in was a success, followed by a complete event.
这个block被传递了一个subscriber实例,实现了RACSubscriber协议,这个协议拥有一些方法,使得你能够调用去发射event;你也能够发射任意数量的
next事件,最后终止于一个error或者complete事件。在这个案例中,它发送了一个next事件,指明了sign-in是否成功,跟随者一个complete事件。
The return type for this block is an
RACDisposable
object, and it allows you to perform any clean-up work that might be required when a subscription is cancelled or trashed. This signal does not have any clean-up requirements, hencenil
is returned.
这个block的返回类型是一个RACDisposable对象,它容许你去执行一些清理工做--当一个
subscription(订阅)被取消或者被丢弃时须要进行的。这里的这个signal没有清理的需求,因此返回了一个nil。
As you can see, it’s surprisingly simple to wrap an asynchronous API in a signal!
就像你所看到的,在一个signal中包裹一个异步的API是使人惊讶的简单容易。
Now to make use of this new signal. Update the code you added to the end of
viewDidLoad
in the previous section as follows:
如今来使用这个signal。更新你刚才增长在viewDidLoad底部的代码:
[[[self.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] map:^id(id x) { return [self signInSignal]; }] subscribeNext:^(id x) { NSLog(@"Sign in result: %@", x); }];
The above code uses the
map
method used earlier to transform the button touch signal into the sign-in signal. The subscriber simply logs the result.
上面的代码使用前面使用过的map方法来把按钮接触的signal转换成一个sign-in的signal,subscriber简单地打印出结果。
If you build and run, then tap the Sign In button, and take a look at the Xcode console, you’ll see the result of the above code …
若是你构造了而且运行,而后点击Sign In按钮,看一下Xcode的控制台,你会看到上面代码的结果...
… and the result isn’t quite what you might have expected!
... 并非你想要的!
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!
这个subscribeNext:
block被传递了一个signal,但不是sign-in signal的结果!
Time to illustrate this pipeline so you can see what’s going on:
到了该说明这个管道的时候了,这样你就能够看到发生了什么:
The
rac_signalForControlEvents
emits a next event (with the sourceUIButton
as its event data) when you tap the button. The map step creates and returns the sign-in signal, which means the following pipeline steps now receive aRACSignal
. That is what you’re observing at thesubscribeNext:
step.
当你点击这个按钮的时候,这个rac_signalForControlEvents发射了一个next事件(伴随着源
UIButton做为它的事件的数据
)。map这一步建立而且返回这个
sign-in signal,这意味着后面的管道这一步如今能够接收一个RACSignal。这就是你在 subscribeNext: 这一步所观察到的。
The situation above is sometimes called the signal of signals; in other words an outer signal that contains an inner signal. If you really wanted to, you could subscribe to the inner signal within the outer signal’s
subscribeNext:
block. However it would result in a nested mess! Fortunately, it’s a common problem, and ReactiveCocoa is ready for this scenario.
上面这种状况有时候被称为 signal of signals (信号的信号);换句话说,一个外部的signal包含了一个内部的signal。若是你真的想要,你能够订阅在外部的signal的subscribeNext:
块里面的内部的signal。而后它会致使一个嵌套混乱!幸运的是,这是一个很普通的问题,ReactiveCocoa已经为这种场景作好了准备。
The solution to this problem is straightforward, just change the
map
step to aflattenMap
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); }];
This maps the button touch event to a sign-in signal as before, but also flattens it by sending the events from the inner signal to the outer signal.
这把按钮的点击事件映射成一个sign-in信号,而且把它flattens, 经过发送事件从内部的signal 到外部的signal。(flatten是扁平化的意思,这里直接翻译成扁平化,不太好理解,因此仍是用原词)
Build and run, and keep an eye on the console. It should now log whether the sign-in was successful or not:
编译而且运行,看一下控制台。如今它会输出sign-in是否成功:
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
Exciting stuff!
使人兴奋吧!
Now that the pipeline is doing what you want, the final step is to add the logic to the
subscribeNext
step to perform the required navigation upon successful sign-in. Replace the pipeline with the following:
如今,管道作了你想作的,最后一步是给subscribeNext这一步增长逻辑去执行在成功
sign-in后所须要的导航。替换管道以下:
[[[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]; } }];
The
subscribeNext:
block takes the result from the sign-in signal, updates the visibility of thesignInFailureText
text field accordingly, and performs the navigation segue if required.
这个subscribeNext: 块使用了
sign-in signal的结果,所以更新了signInFailureText文本域的可见性,而且执行了须要的导航。
Build and run to enjoy the kitten once more! Meow!
编译而且运行,再一次去享受这个小猫程序吧!喵!
Did you notice there is one small user experience issue with the current application? When the sign-in service validates the supplied credentials, is should disable the Sign In button. This prevents the user from repeating the same sign-in. Furthermore, if a failed sign-in attempt occurred, the error message should be hidden when the user tries to sign-in once again.
你有没有注意到,当前的程序有一个很小的用户体验问题?当sign-in service确认了所支持的凭证,是应该使Sign In按钮无效。这防止了用户重复登陆。更进一步,若是一个失败的sign-in尝试发生,错误信息应该在用户尝试再次登陆时隐藏。
But how should you add this logic to the current pipeline? Changing the button’s enabled state isn’t a transformation, filter or any of the other concepts you’ve encountered so far. Instead, it’s what is known as a side-effect; or logic you want to execute within a pipeline when a next event occurs, but it does not actually change the nature of the event itself.
可是你应该怎么去给当前的管道增长这个逻辑呢?修改这个按钮的enabled状态并非一个转换,过滤或者任何你到如今为止接触到的概念。取而代之的,它是一个被称为side-effect反作用,这里没有贬义的意思)的概念。
Replace the current pipeline with the following:
像下面这样修改管道:
[[[[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]; } }];
You can see how the above adds a
doNext:
step to the pipeline immediately after button touch event creation. Notice that thedoNext:
block does not return a value, because it’s a side-effect; it leaves the event itself unchanged.
你能够看到上面是如何在按钮点击事件建立以后添加一个doNext:步骤给管道的。注意这个doNext:块不返回值,由于它是一个反作用;它留下了没有修改的事件。
The
doNext:
block above sets the button enabled property toNO
, and hides the failure text. Whilst thesubscribeNext:
block re-enables the button, and either displays or hides the failure text based on the result of the sign-in.
这个doNext:块设置了按钮的enabled属性为NO,而且隐藏了失败的文本。同时subscribeNext:从新使按钮生效,而且根据
sign-in的结果或者显示或者隐藏失败的文本。
It’s time to update the pipeline diagram to include this side effect. Bask in all it’s glory:
是时候更新管道的图来包含这个反作用了。(Bask in all it’s glory这个意思没有翻译,尚未彻底搞清楚)
Build and run the application to confirm the Sign In button enables and disables as expected.
编译而且运行这个程序去确认这个Sign In button像所期待的那样有效或者无效。
And with that, your work is done – the application is now fully reactive. Woot!
而且,伴随着这个,你的工做也完成了-- 这个程序已是彻底的响应式了。
If you got lost along the way, you can download the final project (complete with dependencies), or you can obtain the code from GitHub, where there is a commit to match each build and run step in this tutorial.
若是你没有跟上文章的思路,你能够下载final project最后的完整的工程),或者你能够从GitHub上去获取代码,里面有一个提交是匹配这篇知道里面的每个编译和运行步骤。
Note: Disabling buttons while some asynchronous activity is underway is a common problem, and once again ReactiveCocoa is all over this little snafu. The
RACCommand
encapsulates this concept, and has anenabled
signal that allows you to wire up the enabled property of a button to a signal. You might want to give the class a try.
注意:在进行某些异步活动时禁用按钮是一个常见问题,and once again ReactiveCocoa is all over this little snafu(后一句暂不会翻译)。这个RACCommand封装了这个概念,有一个enabled signal容许你链接一个按钮的enabled属性到一个 signal。
You might want to give the class a try()。
Hopefully this tutorial has given you a good foundation that will help you when starting to use ReactiveCocoa in your own applications. It can take a bit of practice to get used to the concepts, but like any language or program, once you get the hang of it it’s really quite simple. At the very core of ReactiveCocoa are signals, which are nothing more than streams of events. What could be simpler than that?
但愿这个指导能给你一个很好的基础,帮助你开始在你的程序中使用ReactiveCocoa 。这须要你采用更多的练习去熟悉这些概念,就像任何语言或者程序同样,一旦你掌握了,它实际上很简单。ReactiveCocoa的核心是信号,它们只不过是事件流。 有什么比这更简单?
With ReactiveCocoa one of the interesting things I have found is there are numerous ways in which you can solve the same problem. You might want to experiment with this application, and adjust the signals and pipelines to change the way they split and combine.
采用ReactiveCocoa, 一个有意思的事情是,将会有不少种方法你来解决一个问题。你能够在这个程序里面去实验它,调整信号和管道的拆分和合并的方式。
It’s worth considering that the main goal of ReactiveCocoa is to make your code cleaner and easier to understand. Personally I find it easier to understand what an application does if its logic is represented as clear pipelines, using the fluent syntax.
值得思考的是,ReactiveCocoa的主要目标是让你的代码干净和容易被理解。就我我的而言,若是使用流畅的语法将其逻辑表示为清晰的管道,我会发现更容易理解应用程序的做用。
In the second part of this tutorial series you’ll learn about more advanced subjects such as error handing and how to manage code that executes on different threads. Until then, have fun experimenting!
在这篇指导系列的第二部分,你将会学到更多高级的课题,例如错误处理和如何去管理代码去在不一样的线程上执行。在那以前,开心地实验吧!