ReactiveCocoa Tutorial – The Def
原文地址:
https://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt2
如原作者发现有侵权行为可责令我在24小时之内删除,前提是你能看到。
翻译者:毛毛可
ReactiveCocoa框架,允许你在自己的iOS app中使用函数响应式编程(FRP).在本系列教程的第一部分中,你已经学习了如何使用signal发送事件流来替代标准控件的target-action和事件处理.同时你也学习了关于signal的转换,分离和合并.
在系列的第二部分,也就是本章节,你将学习关于ReactiveCocoa更多的高级特性.包括:
- 另外两种事件类型 error 和 completed
- Throttling(节流控制)
- Threading(线程控制)
- Continuations(延续性)
- 等等...
现在开始!
Twitter Instant
通过本指南,你将开发一个叫做Twitter Instant的app(模仿自Google Instant),是一个根据输入内容,刷新搜索结果的Twitter搜索app.
开始项目包括了基本的用户界面和一些凑合的代码,你现在需要开始项目.和第一章节一样,你需要使用CocoaPods获得ReactiveCocoa框架,并整合到自己的项目中.开始项目已经包含了必须的Podfile(文件内更新ReactiveCocoa为3.1.0 pod 'ReactiveObjC', '3.1.0')文件,现在打开命令行窗口,执行以下命令;
pod install
加入执行过程中没有错误,逆境会看到以下类似的输出:
Analyzing dependencies
Downloading dependencies
Using ReactiveObjc (3.1.0)
Generating Pods project
Integrating client project
之后将生成一份Xcode 工作空间,TwitterInstant.xcworkspace.使用Xcode打开它,确认其中包含两个子项目:
- TwitterInstant:实际逻辑代码的位置(在这里写代码)
- Pods:外部依赖的所在位置,当前只有ReactiveCocoa
构建并运行app.会看到下面的页面:

花些时间熟悉一下app中的代码.这是一个简单的基于split view controller的app.左半部分是RWSearchFormViewController,在storyboard中为其添加了几个控件,其中将search text field通过outlet连接到控制器中.有半部分是RWSearchResultsViewController,一个UITableViewController的子类.
如果你打开RWSearchFormViewController.m文件,你就会发现viewDidLoad
方法中,将split view controller的 detail view controller赋值给了当前的全局私有变量resultsViewController
.本app的主要逻辑都是集中在RWSearchFormViewController类中,并且也会提供给RWSearchResultsViewController搜索数据.
Validating the Search Text
首先,你要确保search text中文本长度超过两个时才有效.如果你之前有完成本系列的第一章节,那么很简单.在RWSearchFormViewController.m中的viewDidLoad
方法后面添加下面代码:
- (BOOL)isValidSearchText:(NSString *)text {
return text.length > 2;
}
这个方法简单的判断了当前文本的长度是否大于二.就像如此简单的逻辑,你可能会问"为什么要将此逻辑独立成方法?"
当前的逻辑的确很简单.但如果在未来其中的逻辑变得更加复杂怎么办?
就像上面那样,你应该将它单独成一个方法.而且,上面的方法让你的代码表达更清晰,告诉别人你为什么要检查字符串的长度.优雅的编程,不是吗?
在这个文件的最上面导入ReactiveCocoa框架:
#import <ReactiveCocoa.h>
同时,还是在本文件的viewDidLoad
方法最后添加下面的代码:
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
self.searchText.backgroundColor = color;
}];
想知道上面代码的意思?
- 获得 search text field的 text signal
- 将signal通过验证是否有效转换成对应的background color
- 然后在
subscribeNext:
的block中赋值给自己的backgroundColor
属性
运行app,观察一下,如果当前的搜索文字过短, 代表着一个无效的输入,text field的背景色变为黄色.

如图,对应的管道看起来如此:

rac_textSignal在text field text内容每次发生变化的时候,就会发送一次next事件,并携带当前的text作为block参数.map
操作将text值转换成color,subscribeNext:
操作将对应的color赋值给text field的backgroundColor
属性.
当然,在第一章的时候,你就知道了,对吧?如果还你不知道,你最好先阅读第一篇文章,并练习一下.
在添加Twitter搜索逻辑之前,还有几个有趣的地方要说一下.
Formatting of Pipelines
当你讨论ReactiveCocoa的代码格式时,最普遍的做法是每一个方法调用另起一行,使之垂直.
在下面的图片中,你可以看到在之前的教程中,都遵循这种代码格式:

这样会让你更清晰的查看管道中的每次操作.同样也要稀释每个block的代码行数;只要是代码过多,就应该将其封装成单独的一个私有方法.
不幸的是,Xcode并不支持这种格式的风格,所以你自己可能发现这与自动排版有冲突!
Memory Management
考虑你在TwitterInstantapp中添加的代码,你知道管道是如何创建并被引用的吗?很显然,因为它没有被分配给变量或者属性,它的引用计数并没有增加,它一定会被销毁.是这样的吗?
ReactiveCocoa其中的一种设计模式就是允许隐式管道的编程风格.在你之前写的所有的响应式代码上,这种风格很明显.
为了支持这种方式,ReactiveCocoa自己保持(引用)了全部的singnal.如果它有至少一个订阅者,那么signal就可用.如果所有的订阅者都被移除了,那么signal也会被销毁掉.更多ReactiveCocoa如何内存管理相关的信息,请参考内存管理文档.
最后一个讨论话题:如何取消一个signal的订阅?在completed或error事件后,订阅者会自动将其移除.也可以用通过RACDisposable来手动移除.
RACSignal的订阅方法(如:subscribeNext:
)都会返回一个RACDisposable的实例对象,允许你通过这个实例对象手动的移除订阅.下面是个简单的例子说明:
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];
这种方式并不会经常反生,但是你要知道可以这么做.
提示:如果你创建了一个管道,但是并没有订阅它,那么管道将永远不会执行,这包括任意的副作用操作,比如
doNext:
block.
Avoiding Retain Cycles
ReactiveCocoa框架内部已经做了很多工作,这也意味着你不需要过多的关心signal的内存管理问题,但有一条关于内存管理的问题你需要考虑:
如果你像下面一样添加了响应式代码:
[[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
关键字来获得text field的引用,Block会捕捉并引用作用域内的值,因此如果self
和signal之间存在相互强引用,将会造成循环引用.这会影响self
对象的生命周期.如果它的生命周期和应用的生命周期一样时,那没什么问题.但在一个更复杂的应用中,这会变得十分棘手.
为了避免可能发生的循环引用,Apple的Working With Blocks文档中,建议对self
使用弱引用.当前的代码会变成这样:
__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
变量使用 __weak关键字来标记一个弱引用.注意,subscribeNext:
block中使用beself
变量.代码看起来并不优雅!
ReactiveCocoa框架中有一个简易写法,你可以使用来代替上面的写法.在文件顶部导入下面的代码:
#import "RACEXTScope.h"
使用下面的代码替换掉上面的代码:
@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实在一个扩展库中定义的宏,它们被引入在ReactiveCocoa框架中.@weakify宏会创建一个隐式的弱引用变量(如果你需要可以传入多个变量),@strongify宏会根据之前的@weakify变量创建一个强引用变量.
提示:如果你对@weakify和@strongify真正做了什么很感兴趣,可以在选择Xcode选择Product -> Perform Action -> Preprocess “RWSearchForViewController”.将会预编译这个view controller,你会看到宏展开后的最终输出.
再提示最后一点,当使用实例变量的block时一定小心.block会对捕捉到的(对象)变量强引用.你可以通过打开编译警告来提示你这个问题.在project->build setting搜索search关键字,并找到如下:

好,现在你要准备开始最有趣的部分了:添加真正的功能到你app中!
提示:在之前的教程中就眼尖的读者肯定会注意到,会毫不迟疑的使用RAC宏来消除当前管道流程中的
subscribeNext:
block.如果你发现了这一点,就改变一下.
Requesting Access to Twitter
你将使用Social框架让TwitterInstant应用允许搜索推特相关信息,并使用Account框架来允许访问Twitter账号.关于Social框架的更详细信息,请查看iOS6教程里专栏.
在你添加代码之前,你需要在运行本app上的模拟器或iPad,登录Twitter账号.打开app设置,选择Twitter菜单选项,然后在屏幕的右手边添加你自己的账号:

开始项目已经添加了必备的框架,你只需要导入这些头文件即可.在RWSearchFormViewController.m文件中,添加以下导入配置到文件的最上面:
#import <Accounts/Accounts.h>
#import <Social/Social.h>
然后在下方添加枚举和常量:
typedef NS_ENUM(NSInteger, RWTwitterInstantError) {
RWTwitterInstantErrorAccessDenied,
RWTwitterInstantErrorNoTwitterAccounts,
RWTwitterInstantErrorInvalidResponse
};
static NSString * const RWTwitterInstantDomain = @"TwitterInstant";
用它们来简单的标识错误.
还是在相同的文件中,在声明属性的下面添加:
@property (strong, nonatomic) ACAccountStore *accountStore;
@property (strong, nonatomic) ACAccountType *twitterAccountType;
ACAccountsStore类提供了访问你设备可以连接到的多种媒体账号的API,ACAccountType类表示一种具体的账户类型.
在相同的文件中,添加一下代码到viewDidLoad
方法最后:
self.accountStore = [[ACAccountStore alloc] init];
self.twitterAccountType = [self.accountStore
accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
这段代码创建了accounts store和Twitter account type实例.
当app请求访问社交媒体账号时,用户会看到一个弹框.这是一个异步操作,这正是一个绝佳的使用响应式编程的好地方!
还是同一个文件,添加下面代码:
- (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<RACSubscriber> 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;
}];
}
此方法有下面操作:
- 声明一个error对象,用于当用户拒绝访问.
- 和第一章一样,通过类方法
createSignal:
创建并返回一个RACSignal实例对象. - 通过account store对象请求访问Twitter.在这里,用户会看到一个询问此app是否可以访问Twitter账户的提示.
- 在用户同意或拒绝访问后,signal事件就会发生.如果用户同意访问,在完成回调中会发送一个next事件.如果用户拒绝访问,会发送一个error事件.
如果你能回忆起第一张教程,signal能够发送三种不同类型的事件:
- Next事件
- Completed事件
- Error事件
在signal的整个生命周期内,可能不会发送一次事件,也可能在completed事件或error事件后,发送至少一次next事件.
最后,为了能够使用这个signal,添加代码到viewDidLoad:
方法最后面:
[[self requestAccessToTwitterSignal]
subscribeNext:^(id x) {
NSLog(@"Access granted");
} error:^(NSError *error) {
NSLog(@"An error occurred: %@", error);
}];
此时构建并运行app,将会看到如下提示:

如果点击OK,subscribeNext:
方法block中的打印代码将会出现在控制台,但是如果点击Don't Allow,error block就会执行,并打印error block对应的的代码.
Accounts框架会记录你的设置.所以,如果你想分别测试两种情况,你需要通过iOS模拟器的菜单选项->Reset Content and Setting来重置.这相对有点麻烦,因为你需要重新确认Twitter认证.
Chaining Signals
只要用户同意APP能访问其Twitte账号,应用就需要持续的监听search textfield中内容的变化,来查询twitter.
应用需要等待请求访问Twitter的signal发出的completed事件,然后订阅text field的signal.不同signal链的顺序是一个常见问题,不过ReactiveCocoa将其处理的十分优雅.
在viewDidLoad
方法的最后将当前的pipeline(管道-rac逻辑),替换成:
[[[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返回的参数-signal.这实际上就是通过控制从一个signal到下一个signal.
提示:你已经在此方法之前将self变为弱引用(weakself),所以无需再在本次管道方法(signal方法链)之前使用@weakify(self)了.
then
方法允许error事件通过而不堵塞.所以subscribeNext:error:
的block最终还是接收到"是否允许访问"步骤中发出的错误信息.
构建并运行程序,并允许APP访问账号,你将会看到你在search field中的输入将会显示到控制台.
2014-01-04 08:16:11.444 TwitterInstant[39118:a0b] m
2014-01-04 08:16:12.276 TwitterInstant[39118:a0b] ma
2014-01-04 08:16:12.413 TwitterInstant[39118:a0b] mag
2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi
2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic
2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!
接下来,添加filter
方法到管道中,用来删除无效的字符串搜索.在本案例中,会过滤掉字符串少于三个字符的情况:
[[[[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);
}];
再次运行程序,观察filter
带来的效果:
2014-01-04 08:16:12.548 TwitterInstant[39118:a0b] magi
2014-01-04 08:16:12.628 TwitterInstant[39118:a0b] magic
2014-01-04 08:16:13.172 TwitterInstant[39118:a0b] magic!
当前应用的管道方法如下图:

应用管道最开始是requestAccessToTwitterSignal,然后转换成rac_textSignal.同时next事件会通过filter
方法来到subscription block. 你也可以看到在第一个步骤中发送的任何error事件也会调用subscribeNext:error:
block.
现在你有了一个search text的signal,是时候用它来搜索Twitter了.
Searching Twitter
Social框架是一组访问Twitter Search API的框架.但是,就像你猜测的那样,Social框架并不是响应式的.接下来通过signal调用来封装API.现在,你应该很熟悉了.
在RWSearchFormViewController.m文件中,添加以下方法:
- (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;
}
通过v1.1 REST API ,创建一个搜索Twitter的请求.上面的代码使用"q"作为搜索参数,搜索包含该字符串的tweets响应.你可以阅读关于此搜索API的更多信息,你可以在Twitter API docs中查看其它请求参数.
下一步就是根据这个请求来创建一个signal.还是在当前的文件,添加如下方法:
- (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<RACSubscriber> 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;
}];
}
步骤如下:
- 初始化,定义一组error对象,其中一个表示Twitter账户未登录,另一个表示在执行搜索时的错误.
- 创建singal对象.
- 通过指定搜索字符串调用方法创建请求对象.
- 查询第一个可用的Twitter账户,如果没有查找到,发送error事件.
- 执行查询请求.
- 响应成功处理,解析返回的JSON数据,并在之后发送next事件,然后发送conpleted事件.
- 响应失败处理,发送error事件.
现在来使用这个新的signal!
在本教程的第一章,你学习了如何使用flattenMap
将每个next事件映射新的signal,并订阅它.是时候再次使用此方法了.在viewDidLoad
的最后将flattenMap
方法添加到管道方法中:
[[[[[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);
}];
构建并运行app,在search text field中输入一些文字.只要文字超过三个字符以上,你就会在控制台看到输入的搜索文本.
下面展示了一部分片段:
2014-01-05 07:42:27.697 TwitterInstant[40308:5403] {
"search_metadata" = {
"completed_in" = "0.019";
count = 15;
"max_id" = 419735546840117248;
"max_id_str" = 419735546840117248;
"next_results" = "?max_id=419734921599787007&q=asd&include_entities=1";
query = asd;
"refresh_url" = "?since_id=419735546840117248&q=asd&include_entities=1";
"since_id" = 0;
"since_id_str" = 0;
};
statuses = (
{
contributors = "<null>";
coordinates = "<null>";
"created_at" = "Sun Jan 05 07:42:07 +0000 2014";
entities = {
signalForSearchText:
方法也会发送error事件,回调给subscribeNext:error:
block.你可以相信我的话,不过最好还是亲自测试一下!
在模拟器中打开设置,并选择你的Twitter账号,点击删除账号按钮来退出登录:

如果选择重新运行应用程序,仍然会询问能否访问Twitter账户,但是选择没有账户可用.signalForSearchText
方法会将发送一个error事件,将会打印如下:
2014-01-05 07:52:11.705 TwitterInstant[41374:1403] An error occurred: Error
Domain=TwitterInstant Code=1 "The operation couldn’t be completed. (TwitterInstant error 1.)"
Code=1表示RWTwitterInstantErrorNoTwitterAccounts
错误.对于一个产品型应用,你需要将错误代码,转换成更有用的信息处理,而不仅仅是打印错误信息.
这里已经说明了error事件的重要性;只要signal发送error事件,就会通过error-handling block
执行回调.这就是一个异常流处理.
提示:当Twitter请求返回一个错误时,请执行的专业异常流处理.这里提示一下,将请求参数提示为无效!
Threading
我很确定你会迫不及待的将从Twitter搜索得到的JSON结果展示到UI上,但是在这之前你还需要做最后一件事.想要知道要做什么,你需要好好地思索一下!
在subscribeNext:error:
那一步上打断点:

重新运行app,如果有需要重新确认Twitter访问,输入一些搜索关键字.当程序执行到断点时,应该就像下面这样:

注意debugger中断点的代码不是执行在主线程上的,主线程就上如上图里的Thread 1.一定要记住你只能在主线程上执行UI操作;所以你如果现在此处显示tweets列表,你必须要讲线程切换到主线程.
这里是ReactiveCocoa框架中非常重要的一点.上面展示了signal在哪个线程执行发送事件.尝试在管道方法的其它处也添加断点,你会惊讶的发现他们也都执行在不同的线程上!
你要如何刷新UI呢?使用Operation queue是一个很合适的方式(关于更多教程请查看 How To Use NSOperations and NSOperationQueues),不过ReactiveCocoa提供了解决这个问题的更简单的方式.
更新你的管道方法,在flattenMap:
方法后面添加dliverOn:
方法,如下面代码:
[[[[[[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);
}];
现在重新运行app,并输入一些文本,让app来到当前的断点.你会看到此时你的subscribeNext:error:
block方法会运行在主线程:

只是一个非常简单的操作就将事件流转换到另一个线程上?太不不可思议了!
你可以安全全的更新你的UI操作了!
提示:如果你有看过*RACScheduler这个类,你就会知道将操作分配到不同线程的一系列选择都有不同的优先级,将其延迟添加到管道方法中也是如此.
是时候看看这些tweets了!
Updating the UI
如果你打开RWSearchResultsViewController.h文件,你就会发现此文件中已经存在了一个displayTweets:
方法,此方法会让右侧的view controller来渲染tweets数组.这个实现非常简单,就是一个标准的UItableView dataSource.displayTweets:
方法的唯一参数是包含RWTweet实例的NSArray对象.RWTweet对象也在初始项目中有提供.
在subscibeNext:error:
方法接收到的数据是NSDictionary类型,是响应了signalForSearchWithText:
方法的通过JSON解析的字典.你现在应该这个字典的结构吗?
如果你查看相关的Twitter API文档,你会看到一个很简单的响应结构,此NSDictionary对象就是此种结构,你会看到一个key叫做statuses的属性是一个tweets数组.
如果你有看过RWTweet类,在头文件中有个tweetWithStatus:
方法,接收一个指定结构的NSDictionary对象,.所以你只需要写一个循环,遍历这个数组,创建RWTweet对象.
不过,你并不需要这么做!这有个更简单的方法.
本次教程是关于ReactiveCocoa和函数式编程的.直接使用对象转换的功能性API会变得更简洁.你需要使用LinqToObjectiveC框架.
关闭TwitterInstant工作空间,打开Podfile文件,编辑更新如下依赖:
platform :ios, '7.0'
pod 'ReactiveObjc', '3.1.0'
pod 'LinqToObjectiveC', '2.0.0'
打开命令行窗口进入当前文件夹,执行下面的命令:
pod updatepod update
你会看到类似下面的输出:
Analyzing dependencies
Downloading dependencies
Installing LinqToObjectiveC (2.0.0)
Using ReactiveObjc (3.1.0)
Generating Pods project
Integrating client project
重新打开workspace文件,证明新pod安装正确,就会像下图:

打开RWSearchFormViewController.m文件,并导入下面头文件到文件顶部位置:
#import "RWTweet.h"
#import "NSArray+LinqExtensions.h"
NSArray+LinqExtensions.h文件来自于LinqToObjectiveC库,其中包括一些列NSArray的方法,允许你使用简单的API转换,排序,分组和过滤数据.
现在就开始使用这些API...更新viewDidLoad
最后的管道方法如下:
[[[[[[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);
}];
正如你看到的,subscribeNext:
block会首先得到tweets数组.linq_select
方法,通过block中的代码,将一个NSDictionary数组转换成是一个RWTweet数组.
转换成功后,就会将tweets数组发送给 results view controller.
构建并运行app,你最终会看到tweets显示在屏幕上:

提示:ReactiveCocoa和LinqToObjectiveC都有着相似的代码思想.ReactiveCocoa同时也吸取了Microsoft’s Reactive Extensions library的思想,LinqToObjectiveC在它们的语言基础上整合的Query APIs 和 LINQ,特别是Linq to Objects.
Asynchronous Loading of Images
你很可能已经注意到了,每个tweet的左边还有一块留白,这个位置是显示Twitter用的头像的.
RWTweet类已经拥有了一个profileImageUrl
属性,用于获取头像.为了让table view 平滑的滚动,你需要确保下载图片的操作不是在主线程上执行.你可以使用GCD或NSOperationQueue.但为什么不使用ReactiveCocoa呢?
打开RWSearchResultsViewController.m
文件,并添加下面的方法到文件中:
-(RACSignal *)signalForLoadingImage:(NSString *)imageUrl {
RACScheduler *scheduler = [RACScheduler
schedulerWithPriority:RACSchedulerPriorityBackground];
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imageUrl]];
UIImage *image = [UIImage imageWithData:data];
[subscriber sendNext:image];
[subscriber sendCompleted];
return nil;
}] subscribeOn:scheduler];
}
You should be pretty familiar with this pattern by now!
你现在应该很熟悉这种设计模式了!
上面的代码首先获得一个后台调度器,来让此signal执行到非主线程上.接下来,创建一个下载image data的signal,并在被订阅者订阅时创建一个UIImage对象.最后一部分就是神奇的subscribeOn:
方法,它可以确保signal执行到指定的调度器上.
是不是很神奇!
现在,还在相同的稳重中更新tableView:cellForRowAtIndex:
方法,在该方法返回之前添加下面代码:
cell.twitterAvatarView.image = nil;
[[[self signalForLoadingImage:tweet.profileImageUrl]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(UIImage *image) {
cell.twitterAvatarView.image = image;
}];
上面的代码,首先会在cell复用时重新设置image,因为复用所有cell会有旧数据.然后创建对应的signal来获取image data.在deliverOn:
这一步,你之前有遇到过,会将下一个事件执行到主线程,所以可以放心的执行subscribeNext:
block.
很简单有效!
构建并运行app,就看到头像了:

Throttling
你可能注意到了,每当你输入一个新字符,就会立即执行搜索Twitter.如果你快速的输入(或者就是简单的一直按住删除键),在一瞬间就会执行好几次搜索.这种效果很不好:首先,你在疯狂搜索TwitterAPI时,同时也在抛弃大量的搜索结果.第二,你不断地更新结果,会让用户分心!
更合适的方法是在serach文本没有变化后的一段时间后,再执行搜索,比如500毫秒.
你也可能猜到了,ReactiveCocoa也提供了简单的方式.
打开RWSearchFormViewController.m
文件,并更新viewDidLoad
最后的管道方法,在filter
后面添加throttle
:
[[[[[[[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
操作只会发送一次next事件.就是这么简单!
构建并运行app,确认一下,止呕当你停止输入的500毫秒后,才会更新搜索结果.是不是感觉好多了?你的用户也这么认为.
你已经完成了Twitter Instant应用.
如果你对弈这份教程有一些疑惑,你可以下载最终项目(别放了在打开项目之前,先执行 pod install
),你也可以从GitHub上获取代码,github上会有每一次构建&运行的commit.
Wrap Up
Before heading off and treating yourself to a victory cup of coffee, it’s worth admiring the final application pipeline:
在举杯庆祝之前,最后再看一下应用管道:

这就是整个数据流了,所有的表达式都很简明的线上到一个管道上.看上去很优雅.你能想象如果不使用响应式技术会变得多么复杂吗?在这样的程序中查看数据流会变得多难?听起来很恐怖,现在你不需要在这么做了!
ReactiveCocoa让一切都变得更好.
最后一点,ReactiveCocoa一般都会使用Model View ViewModel或叫做MVVM设计模式,此模式提供了更好地应用逻辑与视图逻辑的解耦.如果有人对ReactiveCocoa的MVVM该兴趣的话,可以留言告诉我.
我很想听听你们的想法和使用经验!