iOS开发,一篇写得很是好的入门级ReactiveCocoa教程(二)

ReactiveCocoa入门教程——第二部分html

ReactiveCocoa iOS 翻译    2015-05-20 16:37:16    4710    1    1react

本文翻译自RayWenderlich  ReactiveCocoa Tutorial – The Definitive Introduction: Part 2/2ios

 

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

 

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

•另外两个事件类型:errorcompleted编程

•节流json

•线程api

•延伸app

•其余框架

 

是时候深刻研究一下了。

Twitter Instant

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

 

这个应用的初始工程包括一些基本的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 send

相关文章
相关标签/搜索