iOS的函数响应式编程(四)

682

本书译者为: kevinHM

翻译自 leanpub.com/iosfrp

ReactiveCocoa 简介

在上一章我们学习了函数方法:映射(map)、过滤器(filter)以及折叠(fold),在本章中我们将再次熟悉它们,但是这一章是主要是围绕着ReactiveCocoa和函数响应式编程来展开的,所以在开始学习之前需要做一点补充说明。

引入ReactiveCocoa的方式

ReactiveCocoa的引入方式有两种:使用CocoaPods或者作为项目的一个子模块(直接拽入项目中)。虽然ReactiveCocoa官方不支持CococaPods,但是我们可以通过开源社区中提供的相应的服务来使用它。你也可以下载它的2.x版本然后根据官方的介绍对其进行配置,这样做是能够让ReactiveCocoa作为一个子模块引入到项目中的。

使用CocoaPods来引入ReactiveCocoa:打开前面我们创建的Podfile文件,并删除RXCollections行,用pod'ReactiveCocoa', '2.0'替代掉。你的Podfile文件看起来应该是这样的:

platform :ios, "6.0"
target "Playground" do
pod 'ReactiveCocoa' , '2.0'
end

target "PlaygroundTests" do

pod 'ReactiveCocoa' , '2.0'
end

注意:我们使用的是'2.0'版的ReactiveCocoa而非最新的。重新运行podinstall,将从项目中移除RXCollections并引入ReactiveCocoa。项目中任何#import <RXCollections/XXXX>的地方都会编译报错,请把他们也移除。

这一章里面,我们将把代码写在ViewController的实现文件中,而不是在AppDelegate中,所以现在请打开ViewController的实现文件。不要忘记把ReactiveCocoa引入进来 #import <ReactiveCocoa/ReactiveCocoa.h>

ReactiveCocoa的流和序列

ReactiveCocoa中的流是值的序列化的抽象,一个流可以被当成一条通道,而值就是通道中传输的物质,值从通道的一头进入再从另一头出来。只要值从通道的另一头流出来了,我们就可以对过去的所有值进行读取,对于刚刚进入通道的值(即当前值)也是可以读取的。还有比较难理解的就是值的序列化了,那么按照我们当前的理解程度来说,它像是一个数组、一个列表。

事实上,使用rac_sequeuece我们能够轻松地将数组转化为一个流:

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

等一下!Sequences?我以为我们在处理Stream? 好吧,说明一下,Sequences是两种特定类型的流的一种,实际上,RACSequence是一个RACStream的子类。 我们能用流做什么呢?好吧,我将使用流来展示上一章中提到的例子。应用在平方数映射上:

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

注意,跟数组一样,流不能包含nil元素。[译者注:NSArray中以nil作为结束标示,stream也一样]。 非常好!但是流映射后还是流,我们怎么样才能得到数组呢?幸运的是,RACSequence有一个方法返回数组:array。

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

这会打印映射后的数组。比起直接使用RXCollections这多出了几个步骤,但这里我只想说明使用流也可以达成任务。

当然,我们可以合并上面的方法调用来避免污染变量的作用域.

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

总的来说,我们做了这样的事情:

  • 将数组转化成一个序列类型的流。
  • 对流进行映射得到一个新的流。
  • 将新的流转为数组。

序列,默认情况下是延迟加载的(也称:懒加载或被动加载),是pull-driven的,在他们被生成的时候就会提供确切的值,而数组方法会强制给序列的每一个成员赋值。

我们来看一下filtering。为了使用ReactiveCocoa来过滤我们的数组,我们需要再一次把它序列化以便于使用过滤。

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

最后看一下怎么让一个序列流合并为单个值(folding):

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

这种情况下,我们在序列上进行了链式调用,当我们讨论下一节'信号'的时候,(链式调用)是一个关键的概念。

ReactiveCocoa具有左折叠和右折叠的概念。左折叠时折叠算法将从头到尾遍历数组,反之称为右折叠。这样的命名(即左、右折叠)暗示了编程语言对列表的理解,这种概念在Objective-C中是没有的。

确定你现在已经理解了到此为止我们所说的内容,因为这对后面的讲解内容十分重要。

ReactiveCocoa的另外一种流:信号

本篇我们介绍ReactiveCocoa的另一种类型的流,即信号。信号是push-driven的,这点与序列流正好相反,新值虽然能够通过管道进行发布却不能像pull-driven一样在管道中获取,它们所抽象出来的数据只会在以后的某个时间进行传送。

这里需要理解两个概念:pull-driven和push-driven.

Push-driven means that values for the signal are not defined at the moment of signal creation and may become available at a later time (for example, as a result from network request, or any user input). Push-driven : 在创建信号的时候,信号不会被立即赋值,之后才会被赋值(举个栗子:网络请求回来的结果或者是任意的用户输入的结果) Pull-driven means that values in the sequence are defined at the moment of signal creation and we can query values from the stream one-by-one. Pull-driven : 在创建信号的同时序列中的值就会被确定下来,我们可以从流中一个个地查询值。

信号发送三种类型的值:Next Values代表了下一个发送到管道内的值。ErrorValue代表signal无法成功完成,一般很少见,我们会在下一章学习怎么使用她们。CompletionValues代表signal成功完成,我们也会在下一章来学习。这里要注意的是:

一个事情响应中,一个signal(信号)发送了一个Errorvalue或者一个CompletionValue后,就不会再发送任何其他的value. 错误或者成功将只会发送其中一个,绝不会有两个同时发送的情况!

信号是ReactiveCocoa的核心组件之一。ReactiveCocoaUIKit的每一个控件内置了一套信号选择器。例如,UITextField就有一个rac_textSignal,UITextField中每一次按键的响应都会通过它发送出去。下一章我们会学习如何使用信号来执行任务。

ClassDiagram

信号也可以被链接(链式调用)和转化。通过映射或者过滤一个流得到的新的流也可以随后被映射、被过滤,进行所有你能想到的各种操作。在下一章中我们会更多的介绍这方面的内容。

进行ReactiveCocoa信号的订阅 当你随时都想知道某一个值的改变时(不管是next、error或者completion),你就会订阅流——一种最常见的signal。使用信号通常都会有副作用,比如下面这个例子。

我们添加一个textfield控件到viewController's View上,这里我使用storyboard来做,你怎么做随你喜好。

在ViewDidLoad中添加如下代码,订阅textfield的rac_textSignal。

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

创建并运行应用程序,在textField上输入一些内容。每一次每一个新的值输入到textField中,这个Next value就会下发到管道中,然后我们的订阅块就会被执行。

有趣的是,这个特殊的信号不会发送错误值,仅仅在释放的时候发送一个完成值,所以这两个订阅块通常不会被调用。我们可以使用RACSignal上的一个简便的方法subsribeNext:来简化我们的代码:

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

看吧,少了很多代码!

当你订阅一个信号时,实际上你创建了一个“订阅者”,订阅者就好比通道最下方的一个盆,只有放好了盆,通道里面的值才能被接住,通道才能放心的打开。它是自动保留的,并同时保留她订阅的信号,你也可以手动配置订阅者,但这不是一种典型的行为。下一章我们将会学习,当视图复用的时候(像CollectionViewCells 或TableViewCells),如何去有效地配置信号。

ReactiveCocoa状态推导

状态推导是ReactiveCocoa的另一个核心组件。这里不是指类的某个属性(类似于设置一个新的值就代表状态发生了改变),这里我们指的是把属性抽象为流。我们来为上一节的例子增加状态推导。

假设我们的视图是用来创建账户的,我们只允许包含有'@'字符的Email地址,当且仅当,输入的用户名有效时使按键可用,同时我们也希望通过TextField中Text的颜色给用户提供反馈。

  • 首先我们使用IBOutlet在视图上增加一个按键‘button’.

    added_a_button

  • 其次我们将button的enable属性与我们创建的信号绑定。

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

请注意,稍候将看到我们如何使用buttons的命令来更好地约束她的enable属性。

RAC()宏需要两个参数:‘对象’以及这个对象的某个属性的'keyPath'。然后将表达式右边的值和'keyPath'做一个单向的绑定,这个值必须是NSObject类型,所以我们会把boolean量封装成NSNumber。

但是,文本的颜色怎么办?实际上我们在这个基础上做一点点重构就可以了。

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方法之外,我们也不用写任何代码,这也很常见。

ReactiveCocoa怎么使用指令?

在上一节的内容中,我们绑定UIButton的enabled属性并不是最佳实践,因为UIButton增加了一个ReactiveCocoa的类和一条指令。在这一节中我们来介绍ReactiveCocoa的指令。使用button的rac_command可以为我们监控enabled属性。 应用一段ReactiveCocoa的文档:

指令,RACCommand类的代表,创建并订阅动作的信号响应,可以很容易地实现一些用户与应用交互时的边界效果。

指令(行为触发的)通常是UI驱动的,比如按键的点击。指令也可以通过信号自动禁用,这种禁用状态呈现在UI上就是禁用与该指令相关联的任何操作。

当你想要一次用户交互发送一个信号来响应的时候指令就很有用。指令信号对订阅了指令的这个信号而言,她之后的输出都被指令信号所处理。这有一点点混乱,在第五章我们会看到一些指令相关的实践。

现在我们用下面的代码来替代之前的在button上绑定enabled属性的代码

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

任何时候button被点击就会执行signalBlock,rac_command属性会监控使能信号validEmailSignal和button的enabled属性。(实际上,如果我们保留原来的代码,新加这一段会引起重复绑定一个属性的错误)。

另外,这里返回的[RACSignal empty]是什么东西?这里我们需要返回一个信号让属于RACCommand的executionSignal管道(pipe)下发出去。这个信号代表button按下时一些任务需要被处理。在这个处理信号没有返回一个'complete value'('empty '会立即返回一个'complete value')之前button将会保持不可用状态。因为这个例子中我们只是打印了一下,所以这里我们只返回一个empty信号。在第五章我们将继续讨论RACCommand及其用途。

ReactiveCocoa可变状态:RACSubject

在ReactiveCocoa中,RACSubject是一个可变的状态,它是一个你可以主动发送新值的信号,所以只有出现特殊情况,否则我们不推荐使用RACSubject。

下一章我们将学习RACSubject们如何嫁接non-reactivecocoa和reactivecocoa的代码。

ReactiveCocoa热信号与冷信号简介

如果没有人订阅信号,那么信号就不会启动和发送。每次增加一个订阅,就会重复地多发送一个信号,由于用户的操作是琐碎的,所以这种设计没有太大的问题。而在ReactiveCocoa的命名法则中,这种信号被称为“冷(信号)”。但是有的时候我们想要让信号能够立即工作(不需要中间这么繁琐的设置),这样在ReactiveCocoa中被称为“热(信号)”,热信号用的非常少。

这两者的不同是很微妙的,在下一章我们将学习如何利用热信号。

ReactiveCocoa 组播原理

什么是组播?组播是多个订阅者共同享用一个订阅信号的术语。通过上一节的内容我们知道在默认的情况下,信号是冷的。但是有时候,我们并不想让冷信号在每一次被订阅时工作。这通常在边界效应、订阅所要执行的任务代价昂贵或者只能以其他方式在适当的时间执行时有这种需求。这时网络请求浮现在脑海中。

所以与其从这样的信号中创建一个RACMulticastConnection不如使用RACSignal的publish方法或者multicast:方法。前者可以创建一个组播连接,后者也一样为您创建一个组播连接但需要一个RACSubject参数。当它被调用时这个RACSubject可以通过底层信号发送一个值出来。任何对这个值有兴趣的,都可以用这个从底层信号发送一个值到连接的信号来替代你提供的RACSubject,这个信号恰好就等同于你的这个RACSubject.

为了说明这种不同,请参考下面的插图:

multiple_subscriptions

由于信号是冷启动的,每增加一个订阅者,她就会被执行一次。这种情况是我们不希望看到的,可以使用组播连接来改善。

multicast_connection

信号的组播连接订阅,当它传送了一个新值的时候,是通过公共频道来传送给信号的。我们可以根据自己的喜好订阅该信号,但这个信号在订阅相关的操作上有且仅会执行一次,而不是像以前那样增加一个订阅者这个信号上就执行一次订阅相关的操作。

总结

这一章我们讨论的更多的是概念,可能有些难以理解,因为没有在实践的基础上进行讲解,而且还有一些高级概念更是难以掌握。下一章,我们将把本章中学习的知识运用起来,使它们深入人心。我们将要诠释的不仅仅是我们在这里看到的这些,同时我们也将获得一些ReactiveCocoa的最佳实践。