数据驱动与ReactiveCocoa iOS开发
ReactiveCocoa(RAC)是一个objective - c功能反应性编程框架,旨在提供更简洁,这样的代码。 一些有用的框架介绍,你可以看我们的技术讨论,你可以看看一些介绍性的文章在这里,在这里,在这里。 在这篇文章中,我们将假定您熟悉典型ReactiveCocoa术语像溪流,信号,用户,等等。这些资源是伟大的为RAC得到最初的味道,但是如果你渴望更像我们,那么你可能很难找到一篇文章与RAC更全功能的应用程序细节。 为此,我们创建了一个应用程序这使得大量使用RAC。 我们的应用程序演示了如何在应用程序中使用RAC的网络和用户界面层。 一路上,我们会强调什么我们认为RAC,而且也凸显其局限性。 我们认为RAC的主要好处之一是它能够无缝地将应用程序的UI模型和绑定到的数据流,你希望来表示。
开始使用REACTIVECOCOA
第一个开始是给你的项目带来RAC。 你有两个选择。
为了方便起见,我们使用CocoaPods。 我们如果我们没有提到CocoaDocs主机一套精彩的文档RAC。 一定要检查它。
活性堆栈溢出
我们的应用程序利用堆栈溢出的公共API列表网站最大的问题以下平台:Android,iOS,Ruby和Windows Phone。 我们使用一个UITabBarController
视图控制器控制,给它五个标签:每个平台之上,一个和最后一个选项卡整体/热的问题。 每个选项卡的UITabBarController
有一个针对uinavigationcontroller
将允许用户“深入”到每个问题暴露其相应的答案。 因此,虽然相对简单,我们的应用程序展示了如何RAC可以实现解决几个熟悉的编程挑战。
使用RACSIGNAL
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
@H_21_301@。 然而,我们有一个新概念介绍方法的实现:地图:
。地图:
需要一块呼吁每个单元的数据信号。 它返回一个新的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。 注意,删除订阅一个信号用户当用户接收到一个错误:
或完成:
事件。
刷新TABLEVIEW
接下来,我们初始化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
功能的最后一点,我们想强调我们的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提供了一个奇妙的工具套件,当适当地实施,可以使代码更容易阅读的一些元素,更易于维护和更愉快的写。