introduction to reactivecocoa

翻译自:Functional Reactive Programming on iOS – Ash Furrow. 版权所有, 转载请著明出处,保留链接。

Introduction to ReactiveCocoa

我们在上一章学习的map, filer, fold,是函数式方法。在这一章中我们继续使用它们,这一章是关于ReactiveCocoa和函数反应式编程,这方面会着墨多点。

Installing ReactiveCocoa

ReactiveCocoa可以通过两种方式安装,通过CocoaPods或者子模块。官方上ReactiveCocoa不支持CocoaPods安装,但是开源社区提供了CocoaPods的支持,所以我们将使用该方法。如果你想用子模块安装,请跟随其官方向导,并确认是2.0版本。

为了通过CocoaPods安装ReactiveCocoa,请打开上一章我们创建的文件podfile,并且删除RXCollections行,替换成pod 'ReactiveCocoa', '2.0',替换后podfile文件如下。

platform :ios, "6.0"

target "Playgournd" do

pod 'ReactiveCocoa', '2.1.4'

end

target "PlaygourndTests" do

pod 'ReactiveCocoa', '2.1.4'

end

注意到我们使用了2.0版本,并不是最新的版本。重新在命令行里运行pod install, 这会从工程中删除RXCollections,并且安装ReactiveCocoa。 任何#import声明使用RXCollections方法,都将变的非法,请从你的app代理文件中移除它们。
在这章,我们将我我们的实现代码放在视图控制器实现文件中,而不是app代理文件中,请打开视图的实现文件,别玩了增加如下#import语句.

#import <ReactiveCocoa/ReactiveCocoa.h>

###Streans and Sequences(流和序列)
流是一系列值得抽象,你可以把流想象成一根管道,值只能一个一个地进入管道,并且只能一个一个的出来,是线性的,并且不具有储存性,值随着时间而散失。当你处在管道的末端,不可能访问过去的值及现在的值。
一系列的值,恩,有点像链表,在此种情况下,是值数组。事实上,我们可以很容易的通过rac_sequence方法把一个数组NSArray转换成流(Stream)。

NSArray *array = @[@(1), @(2), @(3)];
RACSequence *stream = [array rac_sequence];

等等,序列?我原以为我们处理的是流?是的,是流,序列是流的一种具体形式。事实上,RACSequence是RACStream的一个子类。

不管怎样,我们能对流做些什么操作呢?很好,我将给你演示在流上应用在上一章所讲的同样的方法,来试试平方映射看看。

[stream map:^id(id value)}{
    return @(pow([value intergerValue], 2));
}];

提示:像数组NSArray,流streams也是不能包含nil的。
这很棒,但是map只返回了流,怎么才能返回数组?恩,RACSequence提供了array方法:

NSLog(@"%@", [stream array]);

这会输出我们映射后的数组。这比直接使用RXCollections多做了些工作,但是我想给你展示的是怎样使用流。
当然,可以组合上面的方法,以避免污染局部变量作用域。

NSLog(@"%@", [[[array rac_sequence] map:^id(id value){
    return @(pow([value itegerValue], 2));
})] array]);

这个过程如下,首先我们把数组转换成序列,然后对序列进行映射map,最后再把序列转换回成数组。
序列默认是惰性加载的,是被动驱动的(pull-driven),任何时候只要你它请求,它就会提供值给你。
让我们来看下filtering, 为了使用ReactiveCocoa过滤数组,需要把数组先转换成序列。

NSLog(@"%@", [[[array rac_sequence] filter:^BOOL(id value){
    return [value integerValue] % 2 == 0;
})] array]);

最后,我们来看看在序列上怎么应用折叠。

NSLog(@"%@", [[[array rac_sequence] map:^id(id value) {
    return [value stringValue];
}] foldLeftWithStart:@"" reduce:^id(id accumulator, id value) {
    return [accumulator stringByAppendingString:value];
}]);

这个例子上,你可以看到在序列上的链式操作,在下一节中,这将会是一个关键概念。
ReativeCocoa有左折叠和右折叠之分,左折叠从数组的开始遍历到数组的末尾,而右折叠则从数组的末尾遍历到开始。确保你已经明白这里的所述,因为这对理解后续章节至关重要。

Signals(信号)

信号是另一种流,相比较于序列,信号主动驱动型(push-driven)。推进管道里的值不能被拉出。它抽象了在未来被传送投递的数据。
信号发送三种不同类型的值。Next值是下一个被发送给管道应该出现的值,Error值预示着信号不能成功的完成,这很少发生,我们将在下一章看到。Completion值预示信号已经成功的完成。在下一章,会介绍怎么使用它们。一旦Completion和error值通过信号发送,其他任何值将不会被发送了。除此之外,只有一个值(error 或者 completion)被发送,两个不行。
信号是ReactiveCocoa的核心组件。UIKit组件有内置的信号选择器。例如,UITextField有rac_textSignal,当在文本域中每按下键盘时,文本域的值都将被发送。再下一节,我们将看到怎么使用信号了编码。

class diagram

信号也可以被链式和传递。当映射和过滤一个流时,先创建一个流,这个流先后被映射,过滤等操作,在下一章介绍更多的链式操作。

###Subscriptions(订阅)
在流上订阅,大部分在信号上订阅。一个新的值被发送(可能是next, error, completion),而你想被通知,这时就可以使用订阅。如下例。
在视图控制器上增加一个文本域,并把它连接到一个IBOutlet上。我将用内置的StoryBoard和Assistant Editor来完成此项工作,你可以选择你喜欢的方式完成。

adding a text field

在viewDidLoad方法中增加如下代码,订阅新文本域的rac_textSignal信号。

[self.textField.rac_textSignal subscribeNext:^(id x) {
    NSLog(@"New value: %@", x);
} error:^(NSError *error) {
    NSLog(@"Error: %@", error);
} completed:^{
    NSLog(@"Completed.");
}];

编译运行应用程序并在文本域中输入些文字。在文本域中每输入一个新的值,next的值将会被发送给管道,然后,订阅块将会被执行。

chapter3_1

有意思的是,当信号被释放时,不发送错误值,而只发送完成值,所以它们的订阅块永远不会被执行。根据这一点,我们能够简化代码, 如下:

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

多么简单的代码。
当你订阅一个信号,事实上你也就自动的创建了一个订阅对象,意味着增加对订阅对象的引用计数,并且保持了对信号的引用。你可以人工释放该订阅者,如果你想这样做,不过,这不是典型的方法。在下一章,我们将介绍怎样合理地去释放信号,在在多个视图中复用的时候。

Deriving State(萃取状态)

状态萃取是ReactiveCocoa的另一核心组件。与其当一个状态发生改变时,把类上的一个属性设置新值,不如抽象该属性成一个流。来看看先前的例子,并在上应用状态萃取。
假设我们的视图是一个创账户的表单,并且只有当邮件地址中包含’@’字符,才算合法的输入。当用户输入一个合法的用户名时,按钮的状态才可以按下。并且可通过文本域的文字颜色来给用户以反馈。
首先,我们在视图上增加按钮,并且连接到一个IBOutlet上。

added a button

随后,绑定按钮的enabled属性到一个信号。

RAC(self.button, enabled) = [self.textField.rac_textSignal map:^id(NSString *value) {
    return @([value rangeOfString:@"@"].location != NSNotFound);
}];

随后,我们将使用commands,更优雅的绑定enabled属性。
RAC()宏有两个输入参数:一个是对象,还有一个是对象的属性。无疑,宏会执行右连表达式来绑定。属性值必须是NSObjects类型,这是我们包装boolean成NSNumber的原因。
但是,怎么处理文本域的颜色呢?实际上,可以重用文本域的rac_textSignal,并在上面做一点点改变。

RACSignal *validEmailSignal = [self.textField.rac_textSignal map:^id(NSString *value) {
    return @([value rangeOfString:@"@"].location != NSNotFound);
}];

RAC(self.button, enabled) = validEmailSignal;
RAC(self.textField, textColor) = [validEmailSignal map:^id(id value) {
    if ([value boolValue]) {
        return [UIColor greenColor];
    }
    else {
        return [UIColor redColor];
    }
}];

Invalid email address

valid email address

很棒,看见我们怎么使用validEmailSignal了么?这在ReactiveCocoa中是非常平常的方式。在viewDidLoad方法外,我们没有写其他代码,这也很平常。

###Commands(命令)
刚才,我们提到绑定UIButton的enabled属性不是最好的方法。这是因为UIButton已经被ReactiveCocoa 类别增强,增加了命令(command)。我们将在这一节讨论什么是命令。必须意识到button的rac_command属性会替我们处理好enabled的状态。
引用自ReactiveCocoa document

命令通过RACCommand类表达,作为用户操作动作的回应,它会创建和订阅信号。当用户与应用程序交互时,命令使执行side-effecting非常容易。
通常情况下,用户行为动作触发命令,这是UI驱动的方式,例如一个按钮被点击了。命令也可基于信号自动的失效,并且失效的状态能通过关联的UI能表现出来。

至此,按如下方式,绑定enabled属性。

self.button.rac_command = [[RACCommand alloc] initWithEnabled:validEmailSignal signalBlock:^RACSignal *(id input) {
    NSLog(@"Button was pressed.");
    return [RACSignal empty];
}];

不管什么时候按钮按下,信号块会被执行,rac_command属性处理信号到按钮enabled状态的绑定(事实上,如果我们保留原有的代码,同一个属性将有两个绑定,会引起一个错误)。
但是,返回值是什么?我们需要返回一个信号,它会被发送给RACCommand的executionSignals管道。作为按钮按下的结果,产生信号来做一些工作。直到信号返回complete值(空(empty)会立即返回完成值),按钮才变成可用状态。因为我们记录了按钮按下的结果,所以在这种情况下,我们返回了一个空的信号。
我们将在第五章继续讨论RACCommand。

###RACSubject(目标)
RACSubject是一类非常有趣的信号, 它是ReactiveCocoa世界中的”可变状态”。它是一类你能手工发送新值得信号,基于此,在具体的实例在,它不被推荐使用
在下一章,我们将探寻subjects怎么重写非反应式代码成ReactiveCocoa式的。

###Hot and Cold Signals
信号是惰性的,只有当有人订阅了它们,它们才会做工作和发送信号。每一个附加的订阅,工作将会重做。对不重要的操作,这是可接受的,事实上也是理想的。在ReactiveCocoa的术语中,这种类型的信号是冷的(cold)。
有时,我们想工作立即被处理。这种类型的信号被称为热信号.使用热信号非常少。
它们间的区别非常微妙。我们将在下一章节介绍热信号。

###Multicasting(多播)
多播的概念是指一个信号在多个订阅间共享。默认,信号是冷信号。在有些情况下,冷信号不是理想的(它被订阅了,就每次会执行)。例如,网络请求。
所以,通过使用信号RACSignal上的publish方法,或者multicast:方法,从信号signal创建RACMulticastConnection。RACMulticastConnection方法给我们创建了一个多播连接(multicast connection),multicast:方法也创建了一个多播连接,不过它需要一个输入参数RACSubject。不管什么时候被调用,subject会手动的给潜在的信号发送值,然后任何人如果对信号发送的值感兴趣,就应该订阅该连接的信号。
为了展示区别,请看下面的图。

multiple_subscription

因为信号默认是冷的,所以每次增加一个订阅者,它就会工作一次。在有些情况下,这不能满足要求,所以我们要使用多播连接。

multicast_connection

多播连接从其订阅的信号处创建,当它传递值的时候,将会把值发往信号。你可以订阅该信号很多次,但它所被订阅的工作只做一次。

###Conclusion
在这一章中,我们介绍了很多。有些知识点解释起来比较困难,特别是高深的知识点。在下一章,我们将应用在这章所学到的知识。我们不仅会使用这么这些知识点,还会学习ReativeCocoa实战经验。

翻译自:Functional Reactive Programming on iOS – Ash Furrow. 版权所有, 转载请著明出处,保留链接。