ReactiveCocoa基本组件:深入浅出RACCommand

来源:互联网 发布:天猫和淘宝质量一样吗 编辑:程序博客网 时间:2024/06/04 23:27

什么是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。

1234567
- (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的头文件:

1234567891011
@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的实现文件如下:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
#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最有趣的是创建的时候:

123456789
- (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:方法),另外,这个方法也是一个监视执行流程的一个好地方,比如我们可以这么做:

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

在我们的范例中,按钮自动为我们做了这个操作,我们没有调用-execute:,因此当command执行的时候我们得从其他的属性来获得执行的状态,以便于我们更新UI。executionSignalsRACCommand的一个Signal属性,当每次command开始执行的时候,这个Signal就会发送next:next:发送的参数就是初始化RACCommandsignalBlock,也就是说:它是Signal中的Signal。接下来在mapSubscribeCommandStateToStatusMessage方法中我们初始化一个Signal来表示每当command开始执行的时候返回一个表示开始的字符串:

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

然后再实现一个类似的Signal来表示每当command执行完毕时候转换返回一个字符串:

1234567
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的用法。

其实我们还可以换一个更简单的方式来实现:

1234567
@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:会终止信号。因此我们可以轻松的转换这个错误信息:

123
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有一个executingSignal属性,当execute:调用的时候它会发送YES,而当command终止的时候它会发送NO。如果你只是想得到当前的值可以这么做:

1
BOOL commandIsExecuting = [[command.executing first] boolValue];

如果你在command enabled状态为NO的时候手动调用了-execute:,那么它会立刻发送一个错误,但是这个错误并不会发送到errorsSignal。

-execute:方法会自动订阅Signal并且多播它,也就是说你不用订阅返回的Signal,但是如果你订阅的话也不用担心会产生副作用,也就是执行两次。



0 0
原创粉丝点击