iOS 学习

ReactiveCocoa Tutorial – The Def

2018-06-23  本文已影响1人  毛毛可

原文地址:
https://www.raywenderlich.com/62796/reactivecocoa-tutorial-pt2
如原作者发现有侵权行为可责令我在24小时之内删除,前提是你能看到。
翻译者:毛毛可

ReactiveCocoa框架,允许你在自己的iOS app中使用函数响应式编程(FRP).在本系列教程的第一部分中,你已经学习了如何使用signal发送事件流来替代标准控件的target-action和事件处理.同时你也学习了关于signal的转换,分离和合并.

在系列的第二部分,也就是本章节,你将学习关于ReactiveCocoa更多的高级特性.包括:

现在开始!

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打开它,确认其中包含两个子项目:

构建并运行app.会看到下面的页面:


TwitterInstantStarter.png

花些时间熟悉一下app中的代码.这是一个简单的基于split view controller的app.左半部分是RWSearchFormViewController,在storyboard中为其添加了几个控件,其中将search text field通过outlet连接到控制器中.有半部分是RWSearchResultsViewController,一个UITableViewController的子类.

如果你打开RWSearchFormViewController.m文件,你就会发现viewDidLoad方法中,将split view controllerdetail 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;
  }];

想知道上面代码的意思?

运行app,观察一下,如果当前的搜索文字过短, 代表着一个无效的输入,text field的背景色变为黄色.

15235491618019.png

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


15235492569884.png

rac_textSignaltext field text内容每次发生变化的时候,就会发送一次next事件,并携带当前的text作为block参数.map操作将text值转换成color,subscribeNext:操作将对应的color赋值给text fieldbackgroundColor属性.
当然,在第一章的时候,你就知道了,对吧?如果还你不知道,你最好先阅读第一篇文章,并练习一下.
在添加Twitter搜索逻辑之前,还有几个有趣的地方要说一下.

Formatting of Pipelines

当你讨论ReactiveCocoa的代码格式时,最普遍的做法是每一个方法调用另起一行,使之垂直.

在下面的图片中,你可以看到在之前的教程中,都遵循这种代码格式:


15238012249104.png

这样会让你更清晰的查看管道中的每次操作.同样也要稀释每个block的代码行数;只要是代码过多,就应该将其封装成单独的一个私有方法.

不幸的是,Xcode并不支持这种格式的风格,所以你自己可能发现这与自动排版有冲突!

Memory Management

考虑你在TwitterInstantapp中添加的代码,你知道管道是如何创建并被引用的吗?很显然,因为它没有被分配给变量或者属性,它的引用计数并没有增加,它一定会被销毁.是这样的吗?

ReactiveCocoa其中的一种设计模式就是允许隐式管道的编程风格.在你之前写的所有的响应式代码上,这种风格很明显.

为了支持这种方式,ReactiveCocoa自己保持(引用)了全部的singnal.如果它有至少一个订阅者,那么signal就可用.如果所有的订阅者都被移除了,那么signal也会被销毁掉.更多ReactiveCocoa如何内存管理相关的信息,请参考内存管理文档.

最后一个讨论话题:如何取消一个signal的订阅?在completederror事件后,订阅者会自动将其移除.也可以用通过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关键字,并找到如下:

15244947264230.png

好,现在你要准备开始最有趣的部分了:添加真正的功能到你app中!

提示:在之前的教程中就眼尖的读者肯定会注意到,会毫不迟疑的使用RAC宏来消除当前管道流程中的subscribeNext:block.如果你发现了这一点,就改变一下.

Requesting Access to Twitter

你将使用Social框架让TwitterInstant应用允许搜索推特相关信息,并使用Account框架来允许访问Twitter账号.关于Social框架的更详细信息,请查看iOS6教程里专栏.

在你添加代码之前,你需要在运行本app上的模拟器或iPad,登录Twitter账号.打开app设置,选择Twitter菜单选项,然后在屏幕的右手边添加你自己的账号:


15249342875244.png

开始项目已经添加了必备的框架,你只需要导入这些头文件即可.在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;
  }];
}

此方法有下面操作:

  1. 声明一个error对象,用于当用户拒绝访问.
  2. 和第一章一样,通过类方法createSignal:创建并返回一个RACSignal实例对象.
  3. 通过account store对象请求访问Twitter.在这里,用户会看到一个询问此app是否可以访问Twitter账户的提示.
  4. 在用户同意或拒绝访问后,signal事件就会发生.如果用户同意访问,在完成回调中会发送一个next事件.如果用户拒绝访问,会发送一个error事件.

如果你能回忆起第一张教程,signal能够发送三种不同类型的事件:

在signal的整个生命周期内,可能不会发送一次事件,也可能在completed事件或error事件后,发送至少一次next事件.
最后,为了能够使用这个signal,添加代码到viewDidLoad:方法最后面:

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

此时构建并运行app,将会看到如下提示:


15256208309030.png

如果点击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!

当前应用的管道方法如下图:


15277550256160.png

应用管道最开始是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;
  }];
}

步骤如下:

  1. 初始化,定义一组error对象,其中一个表示Twitter账户未登录,另一个表示在执行搜索时的错误.
  2. 创建singal对象.
  3. 通过指定搜索字符串调用方法创建请求对象.
  4. 查询第一个可用的Twitter账户,如果没有查找到,发送error事件.
  5. 执行查询请求.
  6. 响应成功处理,解析返回的JSON数据,并在之后发送next事件,然后发送conpleted事件.
  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);
  }];

构建并运行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账号,点击删除账号按钮来退出登录:

15280315183153.png

如果选择重新运行应用程序,仍然会询问能否访问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:那一步上打断点:

15289052001225.png

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


15289053432431.png

注意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方法会运行在主线程:

15295931455135.png

只是一个非常简单的操作就将事件流转换到另一个线程上?太不不可思议了!
你可以安全全的更新你的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安装正确,就会像下图:


15296816618466.png

打开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显示在屏幕上:


15296822771514.png

提示: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,就看到头像了:


15296833280709.png

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:

在举杯庆祝之前,最后再看一下应用管道:


15296854029574.png

这就是整个数据流了,所有的表达式都很简明的线上到一个管道上.看上去很优雅.你能想象如果不使用响应式技术会变得多么复杂吗?在这样的程序中查看数据流会变得多难?听起来很恐怖,现在你不需要在这么做了!

ReactiveCocoa让一切都变得更好.

最后一点,ReactiveCocoa一般都会使用Model View ViewModel或叫做MVVM设计模式,此模式提供了更好地应用逻辑与视图逻辑的解耦.如果有人对ReactiveCocoa的MVVM该兴趣的话,可以留言告诉我.

我很想听听你们的想法和使用经验!

上一篇 下一篇

猜你喜欢

热点阅读