原文地址
这几天部门的前辈再用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 enabled
时RACCommand
实例的操作不会被执行。当Command可以执行时,常做的方式是把allowsconcuuent的属性设置为NO,这可以保证Command已经执行完成后不会被重复执行。Command执行的结果是一个RACSignal,因此你可以调用next:
、completed:
、或者error:
。后面将会展示具体使用方式。
Example App
我们假设我们正在设计一个简单的app,其功能是让用户订阅一个邮件。最简单的方式是,用一个UITextField和一个UIButton。当用户输入email并且点击按钮的时候,email地址将会传给某个web服务。看起来很简单,但是我们应该确保用户有最好的体验。如果用户按了两次按钮?`
如何处理请求出错?如果email不合法?
RACCommand`可以帮助我们处理这些情况。在这篇文章中将一步步完善这个小app以此来讨论一些概念和工作原理。
可以从这里获得源码。
从一个非常简单的ViewController可以很好的实践MVVM模式。
1 | - (void)bindWithViewModel { |
在上面的方法(在viewDidLoad
中调用),在View和ViewModel中建立了绑定关系。下面是ViewModel的定义:
1 |
|
如上所示,一个暴露出的RACCommand属性。另外两个是字符串属性,它们和View的两个属性绑定在一起。ViewModel的完整实现如下:
1 |
|
这看起来真是一大坨~看还是从小的地方来看吧。我们真正感兴趣RACCommandRACCommand
创建部分是以下代码:
1 | - (RACCommand *)subscribeCommand { |
Command通过一个enabledSignal
参数来初始化。这个Signal可以指示Command是否可以被执行。在我们本次的用例中Command应该在用户合法输入email时允许被执行。self.emailValidSignal
就是用来在email发生变化发送NO
或者YES
指示的。
signalBlock
参数在Command需要执行时被调用。block应该返回一个signal。当我们设置allowsConcurrentExecution
为NO
,Command将会看守这个signal并且在本次执行未完成前不允许任何新的执行。
由于本次用例中的Command来自于按钮的rac_command
(在UIButtton+RACCommandSupport
分类中定义),根据Command是否可以被执行,按钮会自动切换enabled
和disabled
状态。
当然,Command会在按钮被用户点击的时候自动执行。我们可以通过RACCommand自由的实现这一切。如果你需要手动执行你可以调用-[RACCommand execute:]
,参数是可选的,你可以传递nil。我们的用例里不需要参数,不过这里的参数通常会十分有用(按钮可以将自己当做-execute:
的参数传入)。-execute:
方法也是一个你可以监控执行状态的地方,你可以这样写:
1 | [[self.viewModel.subscribeCommand execute:nil] subscribeCompleted:^{ |
在我们的用例中按钮为我们调用Command的执行(所以我们不需要手动调用-execute:
),所以在Command执行时,为了及时更新UI,我们需要监听Command的另一个属性。有几个让人迷惑的地方,RACCommand
的executionSignals
属性是一个每当Commands开始执行时就发送next:
的Signal。问题在于Signal由Command创建,所以Signal中还有一层Signal。每次Command开始执行的时候, 我们会在ViewModel中通过mapSubscribeCommandStateToStatusMessage
方法里面获取到一个信号。同时在这个信号里面返回了一个字符串:
1 | RACSignal *startedMessageSource = [self.subscribeCommand.executionSignals map:^id(RACSignal *subscribeSignal) { |
假如我们想用更函数的方式,来在Command执行完成后都能获取string,我们需要做更多的工作:
1 | RACSignal *completedMessageSource = [self.subscribeCommand.executionSignals flattenMap:^RACStream *(RACSignal *subscribeSignal) { |
当Command执行时,flattenMap:
方法调用一个带subscribeSignal
参数的block。这个block返回一个新的Signal并且它的值会被传递到下一个返回信号。materialize
操作符让我们捕获到一个RACEvent
(例如 next:
complete
和 error:
都是RACEvent的实例)。我们可以在信号完成之后过滤这些event
并且映射成一个string。这些解释让你晕了吗,不过你可以去看一下flattenMap:和materialize的文档以助于你的理解。
我们可以用另一种不同但更容易理解的方式来实现:
1 | @weakify(self); |
但是我并不喜欢上面的写法,因为这样会block中的操作会更多并且会更多的在block中使用到self
。所以在这里还使用了@weakify
和@strongify
(在libextobjc
中定义)避免循环retain。
关于executionSignals
属性,有一个重要的细节。在这里的Signal所发送的event不包含error
,所以对于那些有特殊errors
属性
1 | RACSignal *failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACScheduler mainThreadScheduler]] map:^id(NSError *error) { |
如果我们有三个带有状态消息的Signal,我们可以将他们合并成一个信号并绑定到ViewModel的一个statusMessage
属性 (statusMessage
绑定ViewController的statusLabel.text)。
1 | RAC(self, statusMessage) = [RACSignal merge:@[startedMessageSource, completedMessageSource, failedMessageSource]]; |
那么以上是一个RACCommand
在iOS app 开发中的一个example。我相信这种实现逻辑比使用UITextFieldDelegate
有更多的优点,能在属性和变量中体现更多的状态。
其他有趣的RACCommand使用细节
RACCommand
有一个executing
属性,实际上它是一个当execute:
时会发送YES
,终止时发送NO
的信号。在订阅信号时这个信号将会发送它的当前值,如果你只需要获取当前值而不需要获得信号,你可以通过以下方式:
1 | BOOL commandIsExecuting = [[command.executing first] boolValue]; |
enabled
属性也是一个发送YES
和NO
的信号。当Command通过发送NO
的enabledSignal
信号创建,或者如果信号在执行并且allowsConcurrentExecutions
为 NO
,enabled
就会发送NO
。
-execute:
方法会自动订阅原始Signal并且广播它。这意味着你不需要去订阅-execute:
返回的信号,但是如果你订阅了也不需要担心它会被执行两次。
有什么问题都可以在博文后面留言,或者微博上私信我。
博主是 iOS 妹子一枚。
希望大家一起进步。
我的微博:LottyLotty周小鱼