编者按:这篇文章是由 霍华德蔓生 和 马特·马赛厄斯 。
ReactiveCocoa (RAC)是一个objective - c功能反应性编程框架,旨在提供更简洁,这样的代码。 一些有用的框架介绍,你可以看我们的 技术讨论 ,你可以看看一些介绍性的文章 在这里 , 在这里 , 在这里。 在这篇文章中,我们将假定您熟悉典型ReactiveCocoa术语像溪流,信号,用户,等等。这些资源是伟大的为RAC得到最初的味道,但是如果你渴望更像我们,那么你可能很难找到一篇文章与RAC更全功能的应用程序细节。 为此,我们创建了一个 应用程序 这使得大量使用RAC。 我们的应用程序演示了如何在应用程序中使用RAC的网络和用户界面层。 一路上,我们会强调什么我们认为RAC,而且也凸显其局限性。 我们认为RAC的主要好处之一是它能够无缝地将应用程序的UI模型和绑定到的数据流,你希望来表示。
第一个开始是给你的项目带来RAC。 你有两个选择。
为了方便起见,我们使用CocoaPods。 我们如果我们没有提到 CocoaDocs 主机一套精彩的 文档RAC。 一定要检查它。
我们的应用程序利用堆栈溢出的公共API列表网站最大的问题以下平台:Android,iOS,Ruby和Windows Phone。 我们使用一个 UITabBarController
视图控制器控制,给它五个标签:每个平台之上,一个和最后一个选项卡整体/热的问题。 每个选项卡的 UITabBarController
有一个 针对uinavigationcontroller
将允许用户“深入”到每个问题暴露其相应的答案。 因此,虽然相对简单,我们的应用程序展示了如何RAC可以实现解决几个熟悉的编程挑战。
RSOTopQuestionsTableViewController
是“主要” UITableViewController
我们的应用程序,它是主屏幕为用户和显示当前堆栈溢出问题。 另一个特定于域的标签以同样的方式工作。 因此,这tableview控制器的一个很好的起点显示RAC提供在我们的项目中。 让我们开始与 -(void)viewDidLoad
并询问以下代码:
@weakify(self);
RACSignal *topQuestionsSignal = [[sharedStore topQuestions] deliverOn:[RACScheduler mainThreadScheduler]];
我们开始与 weakify
宏来避免创建强引用周期 自我
在晚些时候 块 。 相关块内部,我们后续的每个 weakify
与 strongify
。 接下来,我们创建一个实例 RACSignal
作为我们的topQuestionsSignal
。 回想一下,我们使用 RACSignal
捕获和交付现在和未来值push-driven我们关心的数据流。 创建我们的信号,发送消息 topQuestions
对我们的 sharedStore
对象,该对象的单一实例 RSOStore
类,它负责存储我们的首要问题。 在我们继续之前,让我们来绊倒RSOStore.m
检查我们的 topQuestions
方法。
- (RACSignal *)topQuestions
{
RACSignal *signal = [[RSOWebServices sharedServices] fetchQuestionsWithTag:nil];
return [self questionsForSignal:signal];
}
正如我们所看到的,它发送一条消息到我们的web服务单,并返回一个信号。 让我们去拜访RSOWebServices
看看 fetchQuestionsWithTag:
所做的。
- (RACSignal *)fetchQuestionsWithTag:(NSString *)tag
{
NSURL *fetchQuestionURL = [self createRelativeURLWithTag:tag];
@weakify(self);
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
...
这个方法返回一个 RACSignal
。 我们创建的信号 createSignal:
类方法 RACSignal
。createSignal:
需要一块参数这种情况下,参数是一个 ID
这符合 RACSubscriber
protocol-and返回一个 RACDisposable
对象,该对象封装了订阅的拆卸和清理工作。 重要的是要了解的RACSubscriber
协议涉及相关的任务发送用户对象的数据源于他们订阅的信号。
@strongify(self);
NSURLSessionDataTask *task = [self.client dataTaskWithURL:fetchQuestionURL
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if(error)
{
[subscriber sendError:error];
}
else if(!data)
{
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: @"No data was received from the server."};
NSError *dataError = [NSError errorWithDomain:RSOErrorDomain code:RSOErrorCode userInfo:userInfo];
[subscriber sendError:dataError];
}
else
{
NSError *jsonError;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:&jsonError];
if(jsonError)
{
[subscriber sendError:jsonError];
}
else
{
[subscriber sendNext:dict[@"items"]];
[subscriber sendCompleted];
}
}
}];
[task resume];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}];
return signal;
}
里面 createSignal:
块,我们创建URL并启动一个实例 NSURLSessionDataTask
我们的首要问题。 在 completionHandler
块为 NSURLSessionDataTask
,我们 sendError:
到 用户
如果有一个错误下载数据。 否则,如果有数据,我们发送 sendNext:
——我们的数据 sendCompleted
我们的用户在我们已完成下载的问题。 和sendCompleted sendNext sendError:::
所有必需的方法吗 RACSubscriber
协议。 注意,结束的时候 createSignal:
块,我们一定要回报RACDisposable
收拾自己。 在我们的例子中,我们在结束任务的数据 (任务取消)
。 最后,我们返回产生的信号 fetchQuestionsWithTag:
方法。 手里拿着这个信号,我们返回 topQuestions
方法 RSOStore.m
。 我们称之为 (自我questionsForSignal信号):
,小心翼翼地传入我们的新信号,填写的数据模型tableview我们的首要问题。 questionsForSignal:
实现如下:
- (RACSignal *)questionsForSignal:(RACSignal *)signal
{
return [signal map:^(NSArray *questionDicts) {
NSMutableArray *questions = [[NSMutableArray alloc]init];
for(NSDictionary *questionDictionaryItem in questionsDicts)
{
RSOQuestion *question = [RSOQuestion questionForDictionary:questionDictionaryItem];
[questions addObject:question];
}
return [questions copy];
}];
}
就像我们上面所讨论的其他方法,我们返回一个 RACSignal
。 然而,我们有一个新概念介绍方法的实现: 地图:
。 地图:
需要一块呼吁每个单元的数据信号。 它返回一个新的 RACSignal
返回的值块。 在我们的例子中,我们的web服务调用将发送一个JSON对象数组代表问题。 数组中的每一项都被表示为一个字典。 我们遍历这些字典,退出问题的数据字典项,并将其在一个可变的数组中。 看看实现的 RSOQuestion
如果您有兴趣进一步的细节。 年底的这一块,我们返回数组的副本持有我们的表的问题。 在这一点上,我们回来 -viewDidLoad
在RSOTopQuestionsTableViewController.m
。 注意,我们确保 topQuestionsSignal
在主线程通过交付吗 [RACScheduler mainThreadScheduler]
方法的参数 deliverOn:
。 我们是为了避免重载tableview从后台线程一旦我们从我们的信号接收问题。 现在,我们有了自己的信号,我们必须使用它。
[topQuestionsSignal subscribeNext:^(NSArray *questions) {
@strongify(self);
[self loadQuestions:questions];
} error:^(NSError *error) {
[self displayError:[error localizedDescription] title:@"An error occurred"];
[progressOverlay hide:YES];
} completed:^{
[progressOverlay hide:YES afterDelay:1];
}];
topQuestionsSignal
发送 - subscribeNext:错误:完成:
。 这个方法取三块每个执行特定场景的参数,并返回一个 RACDisposable
对象来帮助清理的信号。 subscribeNext:
每次执行其块信号发送一个新值的管道。 在这种情况下,发送的信号值数组tableview的问题。 因此,我们执行方法重载tableview。 错误:
将执行块的场景,我们的信号返回一个错误;例如,失去网络连接。 的 完成:
块给我们一个机会来执行一些工作,将在完成执行我们的信号。 我们借此机会隐藏自己的进步HUD。 注意,删除订阅一个信号用户当用户接收到一个 错误:
或 完成:
事件。
接下来,我们初始化 UIRefreshControl
处理下啤酒刷新表的问题列表。 记住,我们之前的订阅topQuestionsSignal
结论(与新问题数据或与一个错误),但这并不意味着我们不能重用它。
UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init];
[[refreshControl rac_signalForControlEvents:UIControlEventValueChanged] subscribeNext:^(UIRefreshControl *refreshControl) {
@strongify(self);
[topQuestionsSignal subscribeNext:^(NSArray *questions){
[self loadQuestions:questions];
} error:^(NSError *error) {
[self displayError:error.localizedDescription title:@"An error occurred"];
[refreshControl endRefreshing];
} completed:^{
[refreshControl endRefreshing];
}];
}];
self.refreshControl = refreshControl;
RAC提供了许多有用的功能类别,允许开发人员贪污RAC系统组件上。 在上面的代码中,rac_signalForControlEvents:
来自于类别 RACSignalSupport.h
在 UIControl
。 它允许我们钩到变化 UIRefreshControl
发送事件 UIControlEventValueChanged
每当它被激活。 因此,我们通过控制事件的理由 rac_signalForControlEvents:
。 为了开始监听新数据,我们需要发送 subscribeNext:
产生的信号 rac_signalForControlEvents:
。 subscribeNext:
需要一块,和我们指定一个 UIRefreshControl
块的参数。 当 UIRefreshControl
被激活,在信号发送到订阅者。 在这里,我们借此机会来恢复我们的订阅 topQuestionsSignal
,使这个信号再次“热”。 这样做开始另一个web服务请求堆栈溢出以查看是否有任何新问题可用。 通过这种方式,我们刷新表只需更新订阅预先存在的信号。
功能的最后一点,我们想强调我们的tableview顶部的搜索框。 我们使用这个搜索框过滤表的内容与用户输入的查询字符串文本字段。 搜索框使用类别 RACSignalSupport.h
RAC提供上UITextField
。 这一类收益率调用一个实例方法 -(RACSignal *)rac_textSignal
创建并返回一个接收信号 UITextField
。 下面的代码是我们的方法:
RACSignal *searchBoxSignal = [[[self.searchBox rac_textSignal] throttle:kSearchQueryThrottle] skip:1];
RAC(self,filteredTopQuestions) =
[RACSignal combineLatest:@[searchBoxSignal, topQuestionsSignal]
reduce:^id(NSString *filterString, NSArray *questions) {
@strongify(self);
if ([filterString length] > 0)
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"text contains %@", filterString];
self.filteredTopQuestions = [questions filteredArrayUsingPredicate:predicate];
return self.filteredTopQuestions;
}
else
{
return questions;
}
}];
返回的信号 rac_textSignal
开始“热”与当前文本,所以我们选择 跳过:
第一个值。 因此,新值输入到文本字段发送的信号只有在用户开始添加文本。 因为我们不想过滤表太快,我们选择 节流:
的信号 kSearchQueryThrottle
(这是一个常数,我们设置等于0.6秒)。 的理由 节流:
以一个NSTimeInterval
只有发送 下一个
年代后用户给定的时间间隔。 确保跳过on-subscription事件和节流未来事件可以让我们重新加载适当的表。 RAC()
是一个宏,它将创建一个单向绑定之间产生的信号吗 comebineLatest:减少:
和财产 filteredTopQuestions
在 自我
。combineLatest:减少:
是一个类方法 RACSignal
这需要一系列的信号将作为参数combineLatest
。 的 减少
块,在我们的示例中,将返回一个 NSArray
对应的数组filteredTopQuestions
和接受两个参数对应的返回类型由所代表的数据流信号的combineLatest
上面的论点。 在 减少
块,我们看看 filterString
的 长度
是0,以确保更大filterString
存在。 如果是这样,我们过滤前的问题根据数组 filterString
。 如果没有,我们返回完整的顶级数组生成的问题我们之前我们现有的订阅 topQuestionsSignal
。 总之,我们的使用 combineLatest:减少:
的效果是将两个信号组合成一个数组与过滤的目的我们的首要问题基于指定的标准。
我们已经完成了很多:创建了一些信号,通过通过方法和类,他们做了一些工作,使用结果填写我们的数据模型和改变了我们的UI。 总的来说,这之旅 RSOTopQuestionsTableViewController.m
证明了一般方法每个视图控制器接受代表其内容。 继续玩 应用程序 ! 上面演示介绍一些非常强大的RAC特性:
combineLatest:减少
允许开发人员组合成一个信号。 这样做意味着特定属性,采取我们的例子中,可以设置根据复杂的逻辑取决于一个范围。combineLatest:减少:
允许开发人员将信号与完全不同的逻辑。 例如,您可以 节流:
一个信号, 跳过:
前三个值。 这种组合可以使你的代码更加简洁而灵活。节流:
提供了一个简洁的方式添加延时一个操作或一组操作。 这样做是没有传统的手工管理计时器和线程。sendNext:
增加了更多的控制,因为它提供了灵活性,手动推数据信号。 在这一点上,RACSubject
是一个信号,允许手工管理和提供了更多的灵活性(和责任!)。 看到 在这里 和在这里 对于一些实现细节和其他注意事项。RAC的一个令人兴奋的前景是减轻负担的所有这些艰苦的检查应用程序状态。 移动应用程序尤其数据饥饿和RAC为开发人员提供一组工具,可以匹配我们的模型和用户界面的数据,他们将使用更方便。 基于块的结构也是一个巨大的胜利。 这样方便带来一些成本和风险。 首先,RAC至少是一个小的语法偏离你的代码可以用来打字。 第二,RAC需要流利的各种各样的新概念,以了解如何使用框架来解决问题。 第三,或许最有争议的是,RAC旨在介绍函数式编程objective - c时,它不能完全成功。 例如,RAC无法绑定一个UI元素(如数据收集。 ,tableview)。 这样,你仍然需要使用tableview的数据源方法用数据填充表。 无论是好是坏,不会完全功能的应用程序的代码。 可可并不是这种方式构建;然而,值得一提的是,objective - c的基于块的现在和未来正在并将带我们沿着功能路径。 由开发人员和团队,以确保项目的代码是连贯的,有效的和高效的。 总而言之,我们认为RAC提供了一个奇妙的工具套件,当适当地实施,可以使代码更容易阅读的一些元素,更易于维护和更愉快的写。