什么是RACCommand?
RACCommand
是ReactiveCocoa框架中一个很重要的部分,如果利用好了RACCommand
,它将节省你很多的开发时间,并且让我们的应用更加健壮。
笔者遇到了很多刚接触ReactiveCocoa的开发者,他们一开始并不清楚RACCommand
是如何工作的,并且不知道该怎么使用它。所以写一篇关于RACCommand
简短的介绍会对大家有一点点帮助,在RAC的官方文档中,并没有太多提及RACCommand
的用法,虽然头文件中的注释是一份不错的材料,但是毕竟还是对新手有一点吃力。
RACCommand
代表着与交互后即将执行的一段流程。通常这个交互是UI层级的,比如你点击个Button。RACCommand
可以方便的将Button与enable状态进行绑定,也就是当enable为NO的时候,这个RACCommand
将不会执行。RACCommand
还有一个常见的策略:allowsConcurrentExecution
,默认为NO,也就是是当你这个command正在执行的话,你多次点击Button是没有用的。创建一个RACCommand
的返回值是一个Signal,这个Signal会返回next或者complete或者error。接下来我们来看一个范例。
RACCommand范例
接下来我们将实现一个邮箱订阅的功能,只有一个输入框和一个订阅按钮,当用户在输入框输入正确的邮箱,点击订阅将向服务器发送订阅的邮箱号。虽然看起来是一个很简单的需求,但是我们需要处理的细节还是挺多的,比如用户快速的点击了两次订阅按钮、还有如何捕捉订阅失败、如果这个邮箱是非法的怎么办?如果我们用RACCommand
来处理的话,其实是非常方便的。
另一方面,ReactiveCocoa也是实现iOS中MVVM模式的好框架。因此,在controller中我们来绑定view model。
1 2 3 4 5 6 7 | - (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和view model的绑定。精华部分的实现都放在了view model中,我们先来看看view model的头文件:
1 2 3 4 5 6 7 8 9 10 11 | @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 |
其中两个属性已经被我们在controller中绑定了,剩下的subcribeCommand是我们接下来要重点讲解的,view model的实现文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | #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 |
看起来很多比较头大,我们现在分成一个个小的部分来讲解下,其中关于RACCommand
最有趣的是创建的时候:
1 2 3 4 5 6 7 8 9 | - (RACCommand *)subscribeCommand { if (!_subscribeCommand) { NSString *email = self.email; _subscribeCommand = [[RACCommand alloc] initWithEnabled:self.emailValidSignal signalBlock:^RACSignal *(id input) { return [SubscribeViewModel postEmail:email]; }]; } return _subscribeCommand; } |
初始化的时候我们传入了一个enabledSignal
参数,这个参数决定了command什么时候可以执行。在这个范例中,表示的是当我们输入的邮箱地址合法的时候才能执行。self.emailValidSignal
是一个返回YES或者NO的Signal。
signalBlock
参数将在每次我们需要执行command的时候调用,这个block返回一个Signal,这个Signal代表了之前所说的执行流程。我们之前保持了默认的allowsConcurrentExecution
属性为NO,这就保证了我们在完成执行block之前不会再次执行这个block。
因为在ReactiveCocoa中,UIButton的属性rac_command
定义在了一个UIButtton+RACCommandSupport
类别,UIButton的enable状态是与command的执行过程相关联绑定的。
因此当按钮被点击的时候command将会自动执行。当然,这些都是RACCommand
帮我们自动完成的。当你需要手动调用这个command的时候,可以调用-[RACCommand execute:]
方法,传入的参数是可选的,我们在这个例子中将传入nil(其实是button把自己当做参数传入了-execute:
方法),另外,这个方法也是一个监视执行流程的一个好地方,比如我们可以这么做:
1 2 3 | [[self.viewModel.subscribeCommand execute:nil] subscribeCompleted:^{ NSLog(@"The command executed"); }]; |
在我们的范例中,按钮自动为我们做了这个操作,我们没有调用-execute:
,因此当command执行的时候我们得从其他的属性来获得执行的状态,以便于我们更新UI。executionSignals
是RACCommand
的一个Signal属性,当每次command开始执行的时候,这个Signal就会发送next:
,next:
发送的参数就是初始化RACCommand
时signalBlock
,也就是说:它是Signal中的Signal。接下来在mapSubscribeCommandStateToStatusMessage
方法中我们初始化一个Signal来表示每当command开始执行的时候返回一个表示开始的字符串:
1 2 3 | RACSignal *startedMessageSource = [self.subscribeCommand.executionSignals map:^id(RACSignal *subscribeSignal) { return NSLocalizedString(@"Sending request...", nil); }]; |
然后再实现一个类似的Signal来表示每当command执行完毕时候转换返回一个字符串:
1 2 3 4 5 6 7 | 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); }]; }]; |
flattenMap
方法返回一个新的Signal,并且这个新的Signal它的返回值将传递到最终的Signal中,materialize
操作符允许我们将Signal转换成RACEvent
,接下来我们就可以过滤这些事件,只允许成功事件通过,并且将成功事件转换成一个代表成功的字符串。如果大家对这步有不清楚的地方,可以去看看官方flattemMap和materialize的用法。
其实我们还可以换一个更简单的方式来实现:
1 2 3 4 5 6 7 | @weakify(self); [self.subscribeCommand.executionSignals subscribeNext:^(RACSignal *subscribeSignal) { [subscribeSignal subscribeCompleted:^{ @strongify(self); self.statusMessage = @"Thanks"; }]; }]; |
然而,我并不喜欢上面的实现方式,不仅是因为有副作用,而且对self的引用也很不方便,我们不得不使用@weakify
和@strongify
来避免循环引用。
这儿还有一个关于executionSignals
属性比较重要的知识点,它并不包含error事件,因此有一个专门的errors
属性的Signal,这个Signal会在执行command的任何阶段调用next:
发送错误信息,它并不会发送error:
,因为error:
会终止信号。因此我们可以轻松的转换这个错误信息:
1 2 3 | RACSignal *failedMessageSource = [[self.subscribeCommand.errors subscribeOn:[RACScheduler mainThreadScheduler]] map:^id(NSError *error) { return NSLocalizedString(@"Error :(", nil); }]; |
到现在为止,我们已经有了三个带有返回信息的Signal,因此我们将它们合并到一个新的Signal,并且绑定到view model的statusMessage
属性:
1
| RAC(self, statusMessage) = [RACSignal merge:@[startedMessageSource, completedMessageSource, failedMessageSource]];
|
到这儿,整个RACCommand
的流程就差不多结束了,我认为这种实现方式有很多的优势比起在view controller中使用UITextFieldDelegate
和保存过多的变量或属性。
关于RACCommand的其他兴趣点
RACCommand
有一个executing
Signal属性,当execute:
调用的时候它会发送YES,而当command终止的时候它会发送NO。如果你只是想得到当前的值可以这么做:
1
| BOOL commandIsExecuting = [[command.executing first] boolValue];
|
如果你在command enabled状态为NO的时候手动调用了-execute:
,那么它会立刻发送一个错误,但是这个错误并不会发送到errors
Signal。
-execute:
方法会自动订阅Signal并且多播它,也就是说你不用订阅返回的Signal,但是如果你订阅的话也不用担心会产生副作用,也就是执行两次。