ReactiveCocoa入门教程——第二部分

ReactiveCocoa是一个框架,它能让你在iOS应用中使用函数响应式编程(FRP)技术。在本系列教程的第一部分中,你学到了如何将标准的动做与事件处理逻辑替换为发送事件流的信号。你还学到了如何转换、分割和聚合这些信号。html

 

在本系列教程的第二部分,你将会学到一些ReactiveCocoa的高级功能,包括:react

  • 另外两个事件类型:error 和 completedios

  • 节流git

  • 线程github

  • 延伸编程

  • 其余json

 

是时候深刻研究一下了。设计模式

Twitter Instant

在本教程中你将要开发的应用叫Twitter Instant(基于Google Instant的概念),这个应用能搜索Twitter上的内容,并根据输入实时更新搜索结果。api

 

这个应用的初始工程包括一些基本的UI和必须的代码。和第一部分同样,你须要使用CocoaPods来获取ReactiveCocoa框架,并集成到项目中。初始工程已经包含必须的Podfile,因此打开终端,执行下面的命令:数组

pod install

 

若是执行正确的话,你能看到和下面相似的输出:

Analyzing dependencies Downloading dependencies Using ReactiveCocoa (2.1.8) Generating Pods project Integrating client project

 

这会生成一个Xcode workspcae,TwitterInstant.xcworkspace 。在Xcode中打开它,确认其中包含两个项目:

  • TwitterInstant :应用的逻辑就在这里。

  • Pods :这里是外部依赖。目前只包含ReactiveCocoa。

 

构建运行,就能看到下面的界面:

 

花一些时间来熟悉应用的代码。这个是一个很简单的应用,基于split view controller。左栏是RWSearchFormViewController,它经过storyboard在上面添加了一些UI控件,经过outlet链接了search text field。右栏是RWSearchResultsViewController,目前只是UITableViewController的子类。

 

打开RWSearchFormViewController.m,能看到在viewDidLoad方法中,首先定位到results view controller,而后把它分配给resultsViewController私有属性。应用的主要逻辑都会集中在RWSearchFormViewController,这个属性能把搜索结果提供给RWSearchResultsViewController

 

验证搜索文本的有效性

首先要作的就是验证搜索文本,来确保文本长度大于2个字符。若是你完成了本系列教程的第一部分,那这个应该很熟悉。

 

RWSearchFormViewController.m中的viewDidLoad 下面添加下面的方法:

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

 

这个方法就只是确保要搜索的字符串长度大于2个字符。这个逻辑很简单,你可能会问“为何要在工程文件中写这么一个单独的方法呢?”。

 

目前验证输入有效性的逻辑的确很简单,但若是未来逻辑须要变得更复杂呢?若是是像上面的例子中那样,那你就只须要修改一个地方。并且这样写能让你代码的可读性更高,代码自己就说明了你为何要检查字符串的长度。

 

在RWSearchFormViewController.m的最上面,引入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

  • 将其转换为颜色来标示输入是否有效

  • 而后在subscribeNext:block里将颜色应用到search text field的backgroundColor属性

 

构建运行,观察在输入文本太短时,text field的背景会变成黄色来标示输入无效。

 

用图形来表示的话,流程和下面的相似:

 

当text field中的文字每次发生变化时,rac_textSignal都会发送一个next 事件,事件包含当前text field中的文字。map这一步将文本值转换成了颜色值,因此subscribeNext:这一步会拿到这个颜色值,并应用在text field的背景色上。

 

你应该还记得本系列教程第一部分里这些内容吧?若是忘了,建议你先停在这里,回去看一下第一部分。

 

在添加Twitter搜索逻辑以前,还有一些有意思的话题要说说。

 

格式化代码

当你在探索如何格式化ReactiveCocoa的代码时,惯例是每一个操做新起一行,垂直对齐每一个步骤。

 

在下图中你能看到比较复杂的代码是如何对齐的,这是第一部分教程中的代码。

 

这样对齐能让你很容易的看到每一步的操做。同时你还应该减小每一个block中的代码量,若是block中的代码超过几行时,就应该新写一个私有方法。

 

很不幸的是,Xcode不是很喜欢这种风格的格式化,因此你会发现Xcode的自动缩进逻辑老是和你过不去。 

 

内存管理

看一下你添加到TwitterInstant中的代码,你是否好奇建立的这些管道是如何持有的呢?显然,它并无分配给某个变量或是属性,因此它也不会有引用计数的增长,那它是怎么销毁的呢?

 

ReactiveCocoa设计的一个目标就是支持匿名生成管道这种编程风格。到目前为止,在你所写的全部响应式代码中,这应该是很直观的。

 

为了支持这种模型,ReactiveCocoa本身持有全局的全部信号。若是一个signal有一个或多个订阅者,那这个signal就是活跃的。若是全部的订阅者都被移除了,那这个信号就能被销毁了。更多关于ReactiveCocoa如何管理这一过程,参见文档Memory Management

 

上面说的就引出了最后一个问题:如何取消订阅一个signal?在一个completed或者error事件以后,订阅会自动移除(立刻就会讲到)。你还能够经过RACDisposable 手动移除订阅。

 

RACSignal的订阅方法都会返回一个RACDisposable实例,它能让你经过dispose方法手动移除订阅。下面是一个例子:

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的附加操做。

 

避免循环引用

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和这个信号之间存在一个强引用的话,就会形成循环引用。循环引用是否会形成问题,取决于self对象的生命周期。若是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;     }];

 

在上面的代码中,__weak修饰符使bself成为了self的一个弱引用。注意如今subscribeNext:block中使用bself变量。不过这种写法看起来不是那么优雅。

 

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 语句是在Extended Objective-C库中定义的宏,也被包括在ReactiveCocoa中。@weakify宏让你建立一个弱引用的影子对象(若是你须要多个弱引用,你能够传入多个变量),@strongify让你建立一个对以前传入@weakify对象的强引用。

 

注意:若是你有兴趣了解@weakify 和 @strongify 实际上作了什么,在Xcode中,选择Product -> Perform Action -> Preprocess “RWSearchForViewController”。这会对view controller 进行预处理,展开全部的宏,以便你能看到最终的输出。

 

最后须要注意的一点,在block中使用实例变量时请当心谨慎。这也会致使block捕获一个self的强引用。你能够打开一个编译警告,当发生这个问题时能提醒你。在项目的build settings中搜索“retain”,找到下面显示的这个选项:

 

好了,你已经经过理论的考验,祝贺你。如今你应该可以开始有意思的部分了:为你的应用添加一些真正的功能!

 

注意:大家中一些眼尖的读者,那些关注了上一篇教程的读者,无疑已经注意到能够在目前的管道中移除subscribeNext:block,转而使用RAC宏。若是你发现了这个,修改代码,而后奖励本身一个小星星吧~

 

请求访问Twitter

你将要使用Social Framework来让TwitterInstant应用能搜索Twitter的内容,使用Accounts Framework来获取Twitter的访问权限。关于Social Framework的更详细内容,参见iOS 6 by Tutorials中的相关章节。

 

在你添加代码以前,你须要在模拟器或者iPad真机上输入Twitter的登陆信息。打开设置应用,选择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类能让你访问你的设备能链接到的多个社交媒体帐号,ACAccountType类则表明帐户的类型。

 

仍是在这个文件中,把下面的代码添加到viewDidLoad的最下面:

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

 

上面的代码建立了一个account store和Twitter帐户标识符。

 

当应用获取访问社交媒体帐号的权限时,用户会看见一个弹框。这是一个异步操做,所以把这封装进一个signal是很好的选择。

 

仍是在这个文件中,添加下面的代码:

- (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. 定义了一个error,当用户拒绝访问时发送。

  2. 和第一部分同样,类方法createSignal返回一个RACSignal实例。

  3. 经过account store请求访问Twitter。此时用户会看到一个弹框来询问是否容许访问Twitter帐户。

  4. 在用户容许或拒绝访问以后,会发送signal事件。若是用户容许访问,会发送一个next事件,紧跟着再发送一个completed事件。若是用户拒绝访问,会发送一个error事件。

 

回忆一下教程的第一部分,signal能发送3种不一样类型的事件:

  • Next

  • Completed

  • Error

 

在signal的生命周期中,它可能不发送事件,发送一个或多个next事件,在这以后还能发送一个completed事件或一个error事件。

 

最后,为了使用这个signal,把下面的代码添加到viewDidLoad的最下面:

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

 

构建运行,应该能看到下面这样的提示:

 

若是你点击OK,控制台里就会显示subscribeNext:block中的log信息了。若是你点击Don't Allow,那么错误block就会执行,而且打印相应的log信息。

 

Acounts Framework会记住你的选择。所以为了测试这两个选项,你须要经过 iOS Simulator -> Reset Contents and Settings 来重置模拟器。这个有点麻烦,由于你还须要再次输入Twitter的登陆信息。

 

连接signal

一旦用户容许访问Twitter帐号(但愿如此),应用就应该一直监测search text filed的变化,以便搜索Twitter的内容。

 

应用应该等待获取访问Twitter权限的signal发送completed事件,而后再订阅text field的signal。按顺序连接不一样的signal是一个常见的问题,可是ReactiveCocoa处理的很好。

 

viewDidLoad中当前管道的代码替换成下面的:

[[[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事件的发送,而后再订阅由then block返回的signal。这样就高效地把控制权从一个signal传递给下一个。

 

注意:你在以前的代码中已经把self转成弱引用了,因此就不用在这个管道以前再写@weakify(self)了。

 

then方法会跳过error事件,所以最终的subscribeNext:error:  block仍是会收到获取访问权限那一步发送的error事件。

 

构建运行,而后容许访问,你应该能看到search text 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操做来过滤掉无效的输入。在本例里就是长度不够3个字符的字符串:

[[[[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);     }];

 

 再次构建运行,观察过滤器的工做:

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,最终到达订阅者的block。你还能看到第一步发送的error事件也是由subscribeNext:error:block来处理的。

 

如今你已经有了一个发送搜索文本的signal了,是时候来搜索Twitter的内容了。你如今以为还好吗?我以为应该还不错哦~

 

搜索Twitter的内容

你可使用Social Framework来获取Twitter搜索API,但的确如你所料,Social Framework不是响应式的。那么下一步就是把所需的API调用封装进signal中。你如今应该熟悉这个过程了。

 

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这个搜索参数来搜索Twitter中包含有给定字符串的微博。你能够在Twitter API 文档中来阅读更多关于搜索API和其余传入参数的信息。

 

 下一步是基于这个请求建立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 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;     }];}


分别讲一下每一个步骤:

  1. 首先须要定义2个不一样的错误,一个表示用户尚未添加任何Twitter帐号,另外一个表示在请求过程当中发生了错误。

  2. 和以前的同样,建立一个signal。

  3. 用你以前写的方法,给须要搜索的文本建立一个请求。

  4. 查询account store来找到可用的Twitter帐号。若是没有帐号的话,发送一个error事件。

  5. 执行请求。

  6. 在请求成功的事件里(http响应码200),发送一个next事件,返回解析好的JSON数据,而后再发送一个completed事件。

  7. 在请求失败的事件里,发送一个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);     }];

 

 构建运行,在search text field中输入一些文字。当文本长度超过3个字符时,你应该就能在控制台看到搜索Twitter的结果了。

 

下面是一段你将会看到的数据:

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 = "";         coordinates = "";         "created_at" = "Sun Jan 05 07:42:07 +0000 2014";         entities = {             hashtags = ...

 

signalForSearchText:方法还会发送error事件到subscribeNext:error: block里。你最好本身尝试一下。

 

在模拟中打开设置应用,选择你的Twitter帐户,而后按“Delete Account”删除它。

 

再从新运行应用,如今仍是容许访问用户的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的block。这是一个例外流程。

 

注意:当请求Twitter返回错误时也是一个例外流程,尝试一下,比较简单的方法就是把请求参数改为无效的。

 

线程

我相信你已经想把搜索Twitter返回的JSON值和UI链接起来了,可是在这以前还有最后一个须要作的事情。如今须要稍微作一些探索,来看一下这究竟是什么!

 

subscribeNext:error:中以下图所示的地方加一个断点:

 

从新运行应用。若是须要的话,再次输入Twitter登陆信息。在search field中输入一些内容。当在断点中止时,你应该能看到和下图相似的东西:

 

注意断点停在的代码并无在主线程,也就是截图中的Thread 1中执行。请记住你只能在主线程中更新UI。所以你须要切换线程来在UI中展现微博的列表。

 

这展现了ReactiveCocoa框架很重要的一点。上面显示的操做会在signal最开始发送事件的线程中执行。尝试在管道的其余步骤添加断点,你可能会惊奇的发现它们也是在不一样线程上执行的。

 

因此接下来你要怎么更新UI呢?一般的作法是使用操做队列(参见教程如何使用 NSOperations 和 NSOperationQueues)。可是ReactiveCocoa有更简单的解决办法。

 

像下面的代码同样,在flattenMap:以后添加一个deliverOn:操做:

[[[[[[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);     }]; 

 

如今从新运行,输入一些内容,停在断点。你应该能看到subscribeNext:error:block中的代码如今实在主线程执行了:

 

这是真的吗?一个简单的操做,就把事件流切换到不一样的线程了?真的是太棒了!

 

如今你就能安全地更新UI啦!

 

注意:若是你看一下RACScheduler类,就能发现还有不少选项,好比不一样的线程优先级,或者在管道中添加延迟。

如今要展现那些微博了。

 

更新UI

若是你打开RWSearchResultsViewController.h 就会发现已经有一个displayTweets:方法了,它会让右边的view controller根据提供的微博数组来展现内容。实现很是简单,就是一个标准的UITableView数据源。displayTweets:方法须要的惟一一个参数就是包含RWTweet实例的数组。RWTweet模型已经包含在初始工程里了。

 

subscibeNext:error:里收到的数据目前是在signalForSearchWithText:里由返回的JSON值转换获得的一个NSDictionary。因此你怎么肯定字典里的内容呢?

 

看一下Twitter的API文档,那里有返回值的样例。NSDictionary和这个结构对应,因此你能找到一个叫“statuses”的键,它对应的值是一个包含微博的NSArray,每一个条文也是NSDictionary实例。

 

RWTweet已经有一个类方法tweetWithStatus:,方法从NSDictionary中取得须要的数据。因此你须要的作的就是写一个for循环,遍历数组,为每条微博建立一个RWTweet实例。

 

但咱们此次不这么作。还有更好的方法。

 

这篇文章是关于ReactiveCocoa和函数式编程。若是用函数式API来实现把数据从一个格式转换为另外一个会优雅不少。你将会用到LinqToObjectiveC来完成这个任务。

 

关闭TwitterInstant workspace,而后在文本编辑中打开以前建立的Podfile。加入新的依赖:

platform :ios, '7.0'  pod 'ReactiveCocoa', '2.1.8' pod 'LinqToObjectiveC', '2.0.0'

 

在这个文件中打开终端,输入下面的命令:

pod update

 

能看到输出和下面的相似:

Analyzing dependencies Downloading dependencies Installing LinqToObjectiveC (2.0.0) Using ReactiveCocoa (2.1.8) Generating Pods project Integrating client project 


再次打开workspace,检查新的pod是否和下图同样显示出来:

 

打开RWSearchFormViewController.m,添加下列引用:

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

 

NSArray+LinqExtensions.h头文件是LinqToObjectiveC里的,它为NSArray添加了许多方法,能让你用流式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首先获取包含微博的数组。而后linq_select方法对数组中的每一个元素执行提供的block,来把NSDictionary的数组转换成RWTweet的数组。

 

转换完成就把微博发送给result view controller。

 

构建运行,终于能看到微博展现在UI中了:

 

注意:ReactiveCocoa 和 LinqToObjectiveC 灵感的来源类似。 ReactiveCocoa以微软的 Reactive Extensions 库为模型,而 LinqToObjectiveC 以 Language Integrated Query APIs或者说 LINQ为模型,特别是 Linq to Objects.

 

异步加载图片

你可能注意到了每条微博的左侧有一段空隙,这是用来显示Twitter用户头像的。

 

RWTweet类有一个属性profileImageUrl来存放头像的URL。为了让table view能流畅地滚动,你须要让用URL获取图像的代码不在主线程中执行。你可使用Grand Central Dispatch或者NSOperationQueue来实现。可是为何不用ReactiveCocoa呢?

 

打开RWSearchResultsViewController.m,添加下面的方法:

-(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]; }

 

你如今应该对这个模式已经很熟悉了。

 

上面的方法首先获取一个后台scheduler,来让signal不在主线程执行。而后,建立一个signal来下载图片数据,当有订阅者时建立一个UIImage。最后是subscribeOn:来确保signal在指定的scheduler上执行。

 

太神奇了!

 

如今仍是在这个文件中,在tableView:cellForRowAtIndex:方法的return语句以前添加下面的代码:

cell.twitterAvatarView.image = nil;  [[[self signalForLoadingImage:tweet.profileImageUrl]     deliverOn:[RACScheduler mainThreadScheduler]]     subscribeNext:^(UIImage *image) {         cell.twitterAvatarView.image = image;     }];

 

由于cell是重用的,可能有脏数据,因此上面的代码首先重置图片。而后建立signal来获取图片数据。你以前也遇到过deliverOn:这一步,它会把next事件发送到主线程,这样subscribeNext:block就能安全执行了。

 

这么简单真是好。

 

构建运行,如今头像就能正确地显示出来了:

 

译注:做者在原文评论中针对cell重用的问题更新了代码:

[[[[self signalForLoadingImage:tweet.profileImageUrl]     takeUntil:cell.rac_prepareForReuseSignal]     deliverOn:[RACScheduler mainThreadScheduler]]     subscribeNext:^(UIImage *image) {         cell.twitterAvatarView.image = image;     }];

 

节流

你可能注意到了,每次输入一个字,搜索Twitter都会立刻执行。若是你输入很快(或者只是一直按着删除键),这可能会形成应用在一秒内执行好几回搜索。这很不理想,缘由以下:首先,屡次调用Twitter搜索API,但大部分返回结果都没有用。其次,不停地更新界面会让用户分心。

 

更好的解决方法是,当搜索文本在短期内,好比说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);     }];

 

只有当,前一个next事件在指定的时间段内没有被接收到后,throttle操做才会发送next事件。就是这么简单。

 

构建运行,确认一下当中止输入超过500毫秒后,才会开始搜索。感受比以前好一些吧?你的用户也会这么想的。

 

到如今你的Twitter Instant应用已经完成了。放松一下,旋转,跳跃,闭上眼吧~

 

若是你卡在教程中的某个地方了,能够下载最终的工程(再打开以前别忘记运行pod install)。或者在Github上获取这份代码,每一步的构建运行都有一个commit。

 

译注:最终工程里的代码和文章中的有一些区别。主要是在requestAccessToTwitterSignal方法。

总结

在你准备喝杯咖啡放松一下以前,仍是有必要来总结一下应用最终的管道图:

 

数据流仍是挺复杂的,如今这全都用响应式的管道清晰地表现了出来。若是不用响应式的话,你能想象到这个应用会变得多复杂吗?数据流会变得多混乱吗?听起来就很麻烦,还好你不用这么作了。

 

如今你应该知道ReactiveCocoa有多棒了吧!

 

最后一点,ReactiveCocoa让使用Model View ViewModel,或者说MVVM设计模式成为可能。MVVM能让应用逻辑和视图逻辑更好地分离。若是你想了解更多的话,就来看下一篇教程吧。

相关文章
相关标签/搜索