ReactiveCocoa基本组件:理解和使用RACCommand

原文地址在这里

本文源码:https://github.com/olegam/RACCommandExample

RACCommand

RACCommand是ReactiveCocoa的基本组件之一,能节省开发的大部分时间,同时使得iOS/OS X 应用更健壮。

我看到一部分ReactiveCocoa(以下简单RAC)新人并没有完全理解RACCommand,自然也就不知道怎么用它。所以我写了这个小小介绍性文章,希望能对你的理解有所帮助。RACCommand源文件里的注释写得很不错,不过它并没有给任何例子来说说具体怎么用它,对于RAC的新人来说,只看这些注释还是比较难以理解的。

RACCommand类用于表示事件的执行,一般来说是在UI上的某些动作来触发这些事件,比如点击一个按钮。RACCommand的实例能够决定是否可以被执行,这个特性能反应在UI上,而且它能确保在其不可用时不会被执行。通常,当一个命令可以执行时,会将它的属性allowsConcurrentExecution设置为它的默认值:NO,从而确保在这个命令已经正在执行的时候,不会同时再执行新的操作。命令执行的返回值是一个RACSignal,因此我们能对该返回值进行next:,completed或error:,这在下文会有所展示。

例子应用

现在假设我们要写一个简单的iOS APP,它能让用户订阅邮件列表。我们将其做到最简单:一个输入框和一个按钮。当用户输入了电子邮箱地址、点击订阅按钮后,电子邮箱地址会提交到我们的web服务器上。够简单了吧!然而,我们还是得处理一些边界情况,以提供最好的体验。比如如果用户点击按钮两次怎么办?错误如何处理?如果邮箱地址非法呢?RACCommand能帮助我们处理这些情况。我已经实现了一个小的app来演示本文中所讨论的这些概念。

源码: 本例用了一个非常简单的视图控制器,同样还演示了iOS应用的MVVM模式。

根视图控制器包含了视图,还有view model的实例。

  1. -(void)bindWithViewModel{
  2. RAC(self.viewModel,email)=self.emailTextField.rac_textSignal;
  3. self.subscribeButton.rac_command=self.viewModel.subscribeCommand;
  4. self.statusLabel,text)=RACObserve( }

上面的这个方法(在viewDidLoad中调用)将view以及view model绑定。对于咱们感兴趣的代码在都view model中。看看view model的接口:

    @interfaceSubscribeViewModel:NSObject
  1. @property(nonatomic,strong)RACCommand*subscribeCommand;//writetothisproperty
  2. NSString*email;//readfromthisproperty
  3. NSString*statusMessage;
  4. @end
RACCommand暴露在这里,就是本文接下来要讨论的。另外还有两个被绑定到view上用于展示的字符串。这个view model的完整实现如下:

    #import"SubscribeViewModel.h"
  1. #import"AFHTTPRequestOperationManager+RACSupport.h"
  2. #import"NSString+EmailAdditions.h"
  3. staticNSString*constkSubscribeURL=@"http://reactivetest.apiary.io/subscribers";
  4. @interfaceSubscribeViewModel()
  5. strong)RACSignal*emailValidSignal;
  6. @end
  7. @implementationSubscribeViewModel
  8. -(id)init{
  9. self=[superinit];
  10. if(self){
  11. [selfmapSubscribeCommandStateToStatusMessage];
  12. }
  13. returnself;
  14. void)mapSubscribeCommandStateToStatusMessage{
  15. RACSignal*startedMessageSource=[self.subscribeCommand.executionSignalsmap:^id(RACSignal*subscribeSignal){
  16. returnNSLocalizedString(@"Sendingrequest...",153); background-color:inherit; font-weight:bold">nil);
  17. }];
  18. RACSignal*completedMessageSource=[self.subscribeCommand.executionSignalsflattenMap:^RACStream*(return[[[subscribeSignalmaterialize]filter:^BOOL(RACEvent*event){
  19. returnevent.eventType==RACEventTypeCompleted;
  20. }]map:^idvalue){
  21. returnNSLocalizedString(@"Thanks",153); background-color:inherit; font-weight:bold">nil);
  22. }];
  23. RACSignal*failedMessageSource=[[self.subscribeCommand.errorssubscribeOn:[RACSchedulermainThreadScheduler]]map:^NSError*error){
  24. returnNSLocalizedString(@"Error:(",153); background-color:inherit; font-weight:bold">self,statusMessage)=[RACSignalmerge:@[startedMessageSource,completedMessageSource,failedMessageSource]];
  25. RACCommand*)subscribeCommand{
  26. if(!_subscribeCommand){
  27. @weakify(self);
  28. _subscribeCommand=[[RACCommandalloc]initWithEnabled:self.emailValidSignalsignalBlock:^RACSignal*(idinput){
  29. @strongify(return[SubscribeViewModelpostEmail:self.email];
  30. }
  31. return_subscribeCommand;
  32. +(RACSignal*)postEmail:(NSString*)email{
  33. AFHTTPRequestOperationManager*manager=[AFHTTPRequestOperationManagermanager];
  34. manager.requestSerializer=[AFJSONRequestSerializernew];
  35. NSDictionary*body=@{@"email":email?:@""};
  36. return[[[managerrac_POST:kSubscribeURLparameters:body]logError]replayLazily];
  37. RACSignal*)emailValidSignal{
  38. if(!_emailValidSignal){
  39. _emailValidSignal=[RACObserve(NSString*email){
  40. return@([emailisValidEmail]);
  41. return_emailValidSignal;
  42. @end
  43. 呃,这是个大块头,我们一点一点来看。我们最感兴趣的RACCommand的创建如下:
  44. -(RACCommand*)subscribeCommand{
  45. if(!_subscribeCommand){
  46. self);
  47. _subscribeCommand=[[RACCommandidinput){
  48. self.email];
  49. return_subscribeCommand;
  50. command的初始化方法中有一个enabledSignal参数,这个signal就是用来指名command能否被执行的。在我们的例子中,当用户输入的email地址合法时,它才能被执行。self.emailValidSignal这个signal每当email的文本更新时,会发送NO或YES。

    signalBlock参数在command需要执行时调用,这个block需要返回一个signal用来表示正在执行,之前将allowsConcurrentExecute的值设置为默认值NO,此时command会观察这个signal,而且在这个执行进度完成前,不允许新的执行。

    由于command是button的rac_command的属性(定义在UIButton+RACCommandSupport),这个button的enable状态会根据command能否执行来自动改变。

    当用户点击按钮时,command会自动执行。如果你需要手动执行command,可以发送消息:-[RACCommand execute:],参数是可选的,在我们的例子中,我们不需要手动执行。尽管在我们这里不用,但它还是很有用的,这个-execute:方法是一个查看命令执行的状态的方法,如下:

    [[self.viewModel.subscribeCommandexecute:nil]subscribeCompleted:^{
  1. NSLog(@"Thecommandexecuted");
  2. }];

在我们的例子中,button自动执行command(所以我们不需要调用-execute:),所以我们得监听command的另一个属性,在command执行时更新UI。在这里有几种选择来达到目的,也许会有点小迷惑。executionSignals是RACCommand的signal,每当command开始执行时next:,其参数是由command创建的signal,所以这个executionSignals是一个值为signal的signal。我们在view model的mapSubscribeCommandStateToStatusMessage方法中,在command每次开始执行时得到一个包含字符串值的signal:

    RACSignal*startedMessageSource=[RACSignal*subscribeSignal){
  1. returnNSLocalizedString(@"Sendingrequest...",108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> }];
如果我们想用纯粹的函数在command执行完成后得到一个包含字符串的signal,我们不得不多做一点小工作:

    RACSignal*completedMessageSource=[self.subscribeCommand.executionSignalsflattenMap:^RACStream*(RACSignal*subscribeSignal){
  1. RACEvent*event){
  2. returnevent.eventType==RACEventTypeCompleted;
  3. }]map:^idvalue){
  4. returnNSLocalizedString(@"Thanks",108); list-style:decimal-leading-zero outside; color:inherit; line-height:18px; margin:0px!important; padding:0px 3px 0px 10px!important"> }];


flattenMap:方法在command执行时,会调用block并传入subscribeSignal,这个block会返回一个新的signal,它的值就在这个要返回的signal中。materialize会将一个signal转换为RACEvent信号(将一个signal的next:complete和error:消息转换为RACEvent实例的next:的值)。接下来我们就过滤这些事件,只留下RACEventTypeCompleted完成事件,并将其map成一个字符串值。看懂了没?如果没看懂的话,你最好看一下flattenMap:materialize:是用来做什么的。

我们可以用一个不同的方法来实现上面这种行为,还可能更容易懂:

    [subscribeNext:^(RACSignal*subscribeSignal){
  1. [subscribeSignalsubscribeCompleted:^{
  2. self.statusMessage=@"Thanks";
  3. 我不推荐上面这种方法,因为在block里的是副作用(side effects)。还有,上面的block会referring和retaining self,所以我用了@weakify和@strongify宏(在libextobjc中定义)来避免循环引用。所以为了避免使用这种副作用,还是要尽量采用原始的实现好一些。

    还有一个要重点注意的是executionSignals属性,这个signals不会发送了error事件,而是由errors这个属性来发送的。在一个command的执行期间,如果一个signal发送了error,这会被signals当成一个next:事件发送,而errors属性则会发送这个错误信息。errors属性不会发送error:事件。我们很容易就能将错误信息map成一个字符串消息:

      RACSignal*failedMessageSource=[[self.subscribeCommand.errorssubscribeOn:[RACSchedulermainThreadScheduler]]NSError*error){
    1. returnNSLocalizedString(@"Error:(",43); font-family:Arial; font-size:14px; line-height:26px">现在我们有3个关于状态的signals了,现在要展示给用户看,我们将这个3个signals合并成一个signal,并将其绑定到view model的statusMessage属性上(这会被绑定到view controller的statusLabel.text属性上)。

        RAC(failedMessageSource]];
      以上就是在iOS应用里怎么使用RACCommand的一例子了。我认为这个实现逻辑相比较于在view controller里用UITextFieldDegate实现来说要好得多,因为它不需要各种状态变量和属性。

      版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

      相关推荐


      react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
      我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
      我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
      react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
      mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
      我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
      本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
      上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
      上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
      我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
      在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
      楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
      我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
      前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
      我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
      上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
      开源不易,感谢你的支持,❤ star me if you like concent ^_^
      hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
      本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
      ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom