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

ReactiveCocoa入门教程——第二部分

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

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

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

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

?另外两个事件类型:errorcompleted

?节流

?线程

?延伸

?其他

是时候深入研究一下了。

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

时间: 2024-10-24 01:45:40

iOS开发,一篇写得非常好的入门级ReactiveCocoa教程(二)的相关文章

iOS开发UI篇—常见的项目文件介绍

iOS开发UI篇—常见的项目文件介绍 一.项目文件结构示意图 二.文件介绍 1.products文件夹:主要用于mac电脑开发的可执行文件,ios开发用不到这个文件 2.frameworks文件夹主要用来放依赖的框架 3.test文件夹是用来做单元测试的 4.常用的文件夹(项目名称文件夹) (1)XXXinfo.plist文件(在该项目中为  01-常见文件-Info.plist) 1)简单说明 是配置文件,该文件对工程做一些运行期的配置,非常重要,不能删除. 在旧版本xcode创建的工程中,这

iOS开发UI篇—常见的项目文件介绍 - 文顶顶

原文  http://www.cnblogs.com/wendingding/p/3766249.html iOS开发UI篇—常见的项目文件介绍 一.项目文件结构示意图 二.文件介绍 1.products文件夹:主要用于mac电脑开发的可执行文件,ios开发用不到这个文件 2.frameworks文件夹主要用来放依赖的框架 3.test文件夹是用来做单元测试的 4.常用的文件夹(项目名称文件夹) (1)XXXinfo.plist文件(在该项目中为  01-常见文件-Info.plist) 1)简

iOS开发网络篇—搭建本地服务器

iOS开发网络篇—搭建本地服务器 一.简单说明 说明:提前下载好相关软件,且安装目录最好安装在全英文路径下.如果路径有中文名,那么可能会出现一些莫名其妙的问题. 提示:提前准备好的软件 apache-tomcat-6.0.41.tar eclipse-jee-kepler-SR2-macosx-cocoa-x86_64.tar.gz jdk-8u5-macosx-x64.dmg 二.安装和配置本地服务器环境(java)步骤: (1)在文档路径下,新建一个文件夹(NetWord),解压eclips

iOS开发UI篇—使用嵌套模型完成的一个简单汽车图标展示程序

iOS开发UI篇—使用嵌套模型完成的一个简单汽车图标展示程序 一.plist文件和项目结构图 说明:这是一个嵌套模型的示例 二.代码示例: YYcarsgroup.h文件代码: // // YYcarsgroup.h // 07-汽车展示(高级) // // Created by apple on 14-5-28. // Copyright (c) 2014年 itcase. All rights reserved. // #import <Foundation/Foundation.h> @

iOS开发UI篇—懒加载

iOS开发UI篇—懒加载 1.懒加载基本 懒加载——也称为延迟加载,即在需要的时候才加载(效率低,占用内存小).所谓懒加载,写的是其get方法. 注意:如果是懒加载的话则一定要注意先判断是否已经有了,如果没有那么再去进行实例化 2.使用懒加载的好处: (1)不必将创建对象的代码全部写在viewDidLoad方法中,代码的可读性更强 (2)每个控件的getter方法中分别负责各自的实例化处理,代码彼此之间的独立性强,松耦合 3.代码示例 1 // 2 // YYViewController.m 3

iOS开发网络篇—大文件的多线程断点下载(转)

http://www.cnblogs.com/wendingding/p/3947550.html iOS开发网络篇—多线程断点下载 说明:本文介绍多线程断点下载.项目中使用了苹果自带的类,实现了同时开启多条线程下载一个较大的文件.因为实现过程较为复杂,所以下面贴出完整的代码. 实现思路:下载开始,创建一个和要下载的文件大小相同的文件(如果要下载的文件为100M,那么就在沙盒中创建一个100M的文件,然后计算每一段的下载量,开启多条线程下载各段的数据,分别写入对应的文件部分). 项目中用到的主要

iOS开发网络篇—GET请求和POST请求

iOS开发网络篇—GET请求和POST请求 一.GET请求和POST请求简单说明 创建GET请求 1 // 1.设置请求路径 2 NSString *urlStr=[NSString stringWithFormat:@"http://192.168.1.53:8080/MJServer/login?username=%@&pwd=%@",self.username.text,self.pwd.text]; 3 NSURL *url=[NSURL URLWithString:u

iOS开发UI篇—以微博界面为例使用纯代码自定义cell程序编码全过程(一)

iOS开发UI篇-以微博界面为例使用纯代码自定义cell程序编码全过程(一) 一.storyboard的处理 直接让控制器继承uitableview controller,然后在storyboard中把继承自uiviewcontroller的控制器干掉,重新拖一个tableview controller,和主控制器进行连线. 项目结构和plist文件 二.程序逻辑业务的处理 第一步,把配图和plist中拿到项目中,加载plist数据(非png的图片放到spooding files中) 第二步,字

iOS开发网络篇—NSURLConnection基本使用

iOS开发网络篇—NSURLConnection基本使用 一.NSURLConnection的常用类 (1)NSURL:请求地址 (2)NSURLRequest:封装一个请求,保存发给服务器的全部数据,包括一个NSURL对象,请求方法.请求头.请求体.... (3)NSMutableURLRequest:NSURLRequest的子类 (4)NSURLConnection:负责发送请求,建立客户端和服务器的连接.发送NSURLRequest的数据给服务器,并收集来自服务器的响应数据 二.NSUR