原文地址

点击这里

这几天部门的前辈再用RAC的时候问到一个问题,RACCommand在RAC中具体的作用和起到的功能,到底应该如何应用它。

关于RAC的使用文章非常多,但是大多仅限于介绍和基本的使用方法,很少介绍RAC究竟应该如何优雅的嵌入到项目中。

在查阅资料的时候发现了此篇博文,写的非常细致,所以做了一次搬运工。

另,妹子我的英文属于渣渣系列,所以有什么翻译不当,请一定要指教。

Code

文章中所有代码在这里

RACCommand是你的新伙伴吗?

RACCommand是ReactiveCocoa最精华的部分之一,它可以让你在开发中节约大量的时间并让你的iOS或者OS X app有更强的鲁棒性。

我见过不少刚接触ReactiveCocoa(后文将简写为RAC),还不能完全理解RACCommand是如何工作又不知何时应该使用RACCommand的同学。所以我认为这个小介绍将会很实用,可以给他们带来一些启发。官方文档并没有给出多少如何使用RACCommand的Examples,但是RACCommand头文件的介绍还是很不错的,不过这对刚开始用RAC的同学来说还是太难理解了。

RACCommand类是用于表示一些操作的执行。通常,是由于UI上的一些事件触发了RACCommand的执行。比如当用户按了一个按钮,如果对应RACCommand实例可以被执行,就会执行相应的操作。这使得它很容易和UI进行绑定,同时可以保证当RACCommand处于not enabledRACCommand实例的操作不会被执行。当Command可以执行时,常做的方式是把allowsconcuuent的属性设置为NO,这可以保证Command已经执行完成后不会被重复执行。Command执行的结果是一个RACSignal,因此你可以调用next:completed:、或者error:。后面将会展示具体使用方式。

Example App

我们假设我们正在设计一个简单的app,其功能是让用户订阅一个邮件。最简单的方式是,用一个UITextField和一个UIButton。当用户输入email并且点击按钮的时候,email地址将会传给某个web服务。看起来很简单,但是我们应该确保用户有最好的体验。如果用户按了两次按钮?``如何处理请求出错?``如果email不合法?``RACCommand可以帮助我们处理这些情况。在这篇文章中将一步步完善这个小app以此来讨论一些概念和工作原理。

可以从这里获得源码。

从一个非常简单的ViewController可以很好的实践MVVM模式。

- (void)bindWithViewModel {RAC(self.viewModel, email) = self.emailTextField.rac_textSignal;self.subscribeButton.rac_command = self.viewModel.subscribeCommand;RAC(self.statusLabel, text) = RACObserve(self.viewModel, statusMessage);
}
复制代码

在上面的方法(在viewDidLoad中调用),在View和ViewModel中建立了绑定关系。下面是ViewModel的定义:


@interface SubscribeViewModel : NSObject@property(nonatomic, strong) RACCommand *subscribeCommand;// write to this property
@property(nonatomic, strong) NSString *email;// read from this property
@property(nonatomic, strong) NSString *statusMessage;@end
复制代码

如上所示,一个暴露出的RACCommand属性。另外两个是字符串属性,它们和View的两个属性绑定在一起。ViewModel的完整实现如下:

#import "SubscribeViewModel.h"
#import "AFHTTPRequestOperationManager+RACSupport.h"
#import "NSString+EmailAdditions.h"static NSString *const kSubscribeURL = @"http://reactivetest.apiary.io/subscribers";@interface SubscribeViewModel ()
@property(nonatomic, strong) RACSignal *emailValidSignal;
@end@implementation SubscribeViewModel- (id)init {self = [super init];if (self) {[self mapSubscribeCommandStateToStatusMessage];}return self;
}- (void)mapSubscribeCommandStateToStatusMessage {RACSignal *startedMessageSource = [self.subscribeCommand.executionSignals map:^id(RACSignal *subscribeSignal) {return NSLocalizedString(@"Sending request...", nil);}];RACSignal *completedMessageSource = [self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal *subscribeSignal) {return [[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) {return event.eventType == RACEventTypeCompleted;}] map:^id(id value) {return NSLocalizedString(@"Thanks", nil);}];}];RACSignal *failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACScheduler mainThreadScheduler]] map:^id(NSError *error) {return NSLocalizedString(@"Error :(", nil);}];RAC(self, statusMessage) = [RACSignal merge:@[startedMessageSource, completedMessageSource, failedMessageSource]];
}- (RACCommand *)subscribeCommand {if (!_subscribeCommand) {NSString *email = self.email;_subscribeCommand = [[RACCommand alloc] initWithEnabled:self.emailValidSignal signalBlock:^RACSignal *(id input) {return [SubscribeViewModel postEmail:email];}];}return _subscribeCommand;
}+ (RACSignal *)postEmail:(NSString *)email {AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];manager.requestSerializer = [AFJSONRequestSerializer new];NSDictionary *body = @{@"email": email ?: @""};return [[[manager rac_POST:kSubscribeURL parameters:body] logError] replayLazily];
}- (RACSignal *)emailValidSignal {if (!_emailValidSignal) {_emailValidSignal = [RACObserve(self, email) map:^id(NSString *email) {return @([email isValidEmail]);}];}return _emailValidSignal;
}@end
复制代码

这看起来真是一大坨~看还是从小的地方来看吧。我们真正感兴趣RACCommandRACCommand创建部分是以下代码:

- (RACCommand *)subscribeCommand {if (!_subscribeCommand) {NSString *email = self.email;_subscribeCommand = [[RACCommand alloc] initWithEnabled:self.emailValidSignal signalBlock:^RACSignal *(id input) {return [SubscribeViewModel postEmail:email];}];}return _subscribeCommand;
}
复制代码

Command通过一个enabledSignal参数来初始化。这个Signal可以指示Command是否可以被执行。在我们本次的用例中Command应该在用户合法输入email时允许被执行。self.emailValidSignal就是用来在email发生变化发送NO或者YES指示的。

signalBlock参数在Command需要执行时被调用。block应该返回一个signal。当我们设置allowsConcurrentExecutionNO,Command将会看守这个signal并且在本次执行未完成前不允许任何新的执行。

由于本次用例中的Command来自于按钮的rac_command(在UIButtton+RACCommandSupport分类中定义),根据Command是否可以被执行,按钮会自动切换enableddisabled状态。

当然,Command会在按钮被用户点击的时候自动执行。我们可以通过RACCommand自由的实现这一切。如果你需要手动执行你可以调用-[RACCommand execute:],参数是可选的,你可以传递nil。我们的用例里不需要参数,不过这里的参数通常会十分有用(按钮可以将自己当做-execute:的参数传入)。-execute:方法也是一个你可以监控执行状态的地方,你可以这样写:

[[self.viewModel.subscribeCommand execute:nil] subscribeCompleted:^{NSLog(@"The command executed");
}];
复制代码

在我们的用例中按钮为我们调用Command的执行(所以我们不需要手动调用-execute:),所以在Command执行时,为了及时更新UI,我们需要监听Command的另一个属性。有几个让人迷惑的地方,RACCommandexecutionSignals属性是一个每当Commands开始执行时就发送next:的Signal。问题在于Signal由Command创建,所以Signal中还有一层Signal。每次Command开始执行的时候, 我们会在ViewModel中通过mapSubscribeCommandStateToStatusMessage方法里面获取到一个信号。同时在这个信号里面返回了一个字符串:

RACSignal *startedMessageSource = [self.subscribeCommand.executionSignals map:^id(RACSignal *subscribeSignal) {return NSLocalizedString(@"Sending request...", nil);
}];
复制代码

假如我们想用更函数的方式,来在Command执行完成后都能获取string,我们需要做更多的工作:

RACSignal *completedMessageSource = [self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal *subscribeSignal) {return [[[subscribeSignal materialize] filter:^BOOL(RACEvent *event) {return event.eventType == RACEventTypeCompleted;}] map:^id(id value) {return NSLocalizedString(@"Thanks", nil);}];
}];
复制代码

当Command执行时,flattenMap:方法调用一个带subscribeSignal参数的block。这个block返回一个新的Signal并且它的值会被传递到下一个返回信号。materialize操作符让我们捕获到一个RACEvent(例如 next: completeerror:都是RACEvent的实例)。我们可以在信号完成之后过滤这些event并且映射成一个string。这些解释让你晕了吗,不过你可以去看一下flattenMap:和materialize的文档以助于你的理解。

我们可以用另一种不同但更容易理解的方式来实现:

@weakify(self);
[self.subscribeCommand.executionSignals subscribeNext:^(RACSignal *subscribeSignal) {[subscribeSignal subscribeCompleted:^{@strongify(self);self.statusMessage = @"Thanks";}];
}];
复制代码

但是我并不喜欢上面的写法,因为这样会block中的操作会更多并且会更多的在block中使用到self。所以在这里还使用了@weakify@strongify(在libextobjc中定义)避免循环retain。

关于executionSignals属性,有一个重要的细节。在这里的Signal所发送的event不包含error,所以对于那些有特殊errors属性

RACSignal *failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACScheduler mainThreadScheduler]] map:^id(NSError *error) {return NSLocalizedString(@"Error :(", nil);
}];
复制代码

如果我们有三个带有状态消息的Signal,我们可以将他们合并成一个信号并绑定到ViewModel的一个statusMessage属性 (statusMessage绑定ViewController的statusLabel.text)。

RAC(self, statusMessage) = [RACSignal merge:@[startedMessageSource, completedMessageSource, failedMessageSource]];
复制代码

那么以上是一个RACCommand在iOS app 开发中的一个example。我相信这种实现逻辑比使用UITextFieldDelegate有更多的优点,能在属性和变量中体现更多的状态。

其他有趣的RACCommand使用细节

RACCommand有一个executing属性,实际上它是一个当execute:时会发送YES,终止时发送NO的信号。在订阅信号时这个信号将会发送它的当前值,如果你只需要获取当前值而不需要获得信号,你可以通过以下方式:

BOOL commandIsExecuting = [[command.executing first] boolValue];
复制代码

enabled属性也是一个发送YESNO的信号。当Command通过发送NOenabledSignal信号创建,或者如果信号在执行并且allowsConcurrentExecutionsNOenabled就会发送NO

-execute:方法会自动订阅原始Signal并且广播它。这意味着你不需要去订阅-execute:返回的信号,但是如果你订阅了也不需要担心它会被执行两次。


有什么问题都可以在博文后面留言,或者微博上私信我,或者邮件我 coderfish@163.com。

博主是 iOS 妹子一枚。

希望大家一起进步。

我的微博:小鱼周凌宇

【译文练习】ReactiveCocoa概要:了解和使用RACCommand相关推荐

  1. 【译】MVVM Tutorial with ReactiveCocoa: Part 1/2

    本文由Colin Eberhardt发表于raywenderlich,原文可查看MVVM Tutorial with ReactiveCocoa: Part ½ 你可能已经在Twitter上听过这个这 ...

  2. iOS - CodeReview 代码评审

    1.CodeReview Code Review 中文应该译作 "代码审查" 或是 "代码评审",这是一个流程,当开发人员写好代码后,需要让别人来 review ...

  3. 最快让你上手ReactiveCocoa之进阶篇

    前言 由于时间的问题,暂且只更新这么多了,后续还会持续更新本文<最快让你上手ReactiveCocoa之进阶篇>,目前只是简短的介绍了些RAC核心的一些方法,后续还需要加上MVVM+Rea ...

  4. ReactiveCocoa源码解读(二)

    上一篇解读了ReactiveCocoa的三个重要的类的底层实现,本篇继续. 一.RACMulticastConnection 1.应用 RACMulticastConnection: 用于当一个信号被 ...

  5. 最快让你上手ReactiveCocoa之基础篇

    前言 很多blog都说ReactiveCocoa好用,然后各种秀自己如何灵活运用ReactiveCocoa,但是感觉真正缺少的是一篇如何学习ReactiveCocoa的文章,小编看了很多篇都没看出怎么 ...

  6. iOS开发学无止境 - 这样好用的ReactiveCocoa,根本停不下来

    作者:空之境界 网址:http://supermao.cn/zhe-yang-hao-yong-de-reactivecocoagen-ben-ting-bu-xia-lai/ 前戏 我个人非常推崇R ...

  7. ReactiveCocoa基础

    本文转载自最快让你上手ReactiveCocoa之基础篇,在此基础上稍作修改,欢迎交流. 有关对 ReactiveCocoa 的看法可以看一下唐巧的这篇ReactiveCocoa 讨论会 Reacti ...

  8. ReactiveCocoa入门教程——第一部分

    本文翻译自RayWenderlich,原文:ReactiveCocoa Tutorial--The Definitive Introduction: Part 1/2 作为一个iOS开发者,你写的每一 ...

  9. ReactiveCocoa核心元素与信号流

    ReactiveCocoa(以下简称"RAC")是一个函数响应式编程框架,它能让我们脱离Cocoa API的束缚,给我们提供另外一套编码的思路与可能性,它能在宏观层面上提升代码易读 ...

最新文章

  1. TensorRT-8量化分析
  2. Swift语言实现代理传值
  3. 某多多买菜程序员:最长持续工作时间高达30小时!睁眼就工作,闭眼就睡觉!多多买菜离职率超级高!公司不得不降低门槛持续招人!...
  4. pycharm黄色高亮提示:Default argument value is mutable
  5. 近五年计算机网络技术的发展,计算机网络技术的近期发展
  6. [AHOI2008] 紧急集合
  7. 软件设计师冲刺笔记(一)
  8. 一个新的自己从2009年的第一天...
  9. scala编程第17章学习笔记(2)——集和映射
  10. 东方乐器及音乐、音乐的常识
  11. 使用Visual Studio来创建动态库/静态库,并加载
  12. BZOJ2431:[HAOI2009]逆序对数列(DP,差分)
  13. 安装windows7系统报错
  14. 程序员求职之道(《程序员面试笔试宝典》)之求职的时候该不该只看钱?
  15. 怎样快速打出初中常用的化学反应方程式
  16. Linux系统防火墙概述
  17. 中国土地市场销售规模状况与投资战略研究报告2022版
  18. 关于工程总承包(EPC)项目最高限价的说明
  19. php phalcon 中文手册,基础教程 · Phalcon 3.4中文手册 · 看云
  20. 联想 e460 查看 内存卡槽数

热门文章

  1. 如何配置java环境变量
  2. python中列表 字典 元祖 enumerate()函数
  3. c++程序设计中多态与虚函数知识点
  4. CentOS7服务管理(重启,停止,自动启动命令)
  5. Python错误:TypeError: string indices must be integers
  6. python json按输入顺序输出内容
  7. 10款 非常酷炫的网站首页焦点图 兼容ie浏览器
  8. 缩点【洛谷P1262】 间谍网络
  9. Storm0.9.4安装 - OPEN 开发经验库
  10. 「最有用」的特殊大数据:一文看懂文本信息系统的概念框架及功能