itenyh
itenyh

Reputation: 1939

ReactiveCocoa -- When error is catched the signal can not be triggered again

I try to serialize several asynchronous network with ReactiveCocoa:

[[[[self.profileView.signInButton rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^__kindof RACSignal * _Nullable(__kindof UIControl * _Nullable value) {
    @strongify(self)
    return [self.viewModel signInSignal];
}] flattenMap:^__kindof RACSignal * _Nullable(id  _Nullable value) {
    return [self.viewModel userInfoSignal];
}] subscribeNext:^(id  _Nullable x) {

} error:^(NSError * _Nullable error) {
    NSLog(@"error");
}];

it works if no network error happens. However, when any error happens and the error block is called, I click the 'signInButton' again but it did not work any more. I want to know the reason and how to fix it. Thanks!

Upvotes: 0

Views: 69

Answers (1)

MeXx
MeXx

Reputation: 3357

The reason is that Error events are forwarded immediately through the chain of operators and that Error events terminate a subscription.

One way of dealing with this is using the retry operator which resubscribes to the signal when an error occurs. Heres a modified version of your sample using retry:

@weakify(self);
[[[[[[self.button rac_signalForControlEvents:UIControlEventTouchUpInside] flattenMap:^__kindof RACSignal * _Nullable(__kindof UIControl * _Nullable value) {
  @strongify(self)
  return [self.viewModel signInSignal];
}] flattenMap:^__kindof RACSignal * _Nullable(id  _Nullable value) {
  return [self.viewModel userInfoSignal];
}] doError:^(NSError * _Nonnull error) {
  NSLog(@"Inner Error: %@", error);
}] retry:2]
 subscribeNext:^(id  _Nullable x) {
  NSLog(@"Next: %@", x);
} error:^(NSError * _Nullable error) {
  NSLog(@"Error: %@", error);
}];

Here, a variant with a retryCount is used - it will retry 2 times before propagating the error to the outside. Also, doError is used to perform a logging call as side effect when the error happens (Note: This has to be before the retry operator)

Given the signInSignal or userInfoSignal will produce an error 100% of the time, 3 button presses will produce the following output

Inner Error

Inner Error

Inner Error

Error

RACCommand

Another solution I would suggest you take a look at is to encapsulate the logic into a RACCommand (in your viewModel):

_signInCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
  return [self.signInSignal flattenMap:^RACSignal *(id value) {
    return self.userInfoSignal;
  }];
}];

You can link this to your butto as simple as:

self.button.rac_command = self.viewModel.signInCommand;

This way, you don't need to manually handle retry: Every click on the button will invoke the command one time which can either complete successfully or error.

You can handle the side effects of the command via the commands special signals:

[[self.viewModel.signInCommand errors] subscribeNext:^(NSError * _Nullable x) {
  NSLog(@"Error: %@", x);
}];

[self.viewModel.signInCommand.executionSignals subscribeNext:^(id  _Nullable signal) {
  [signal subscribeNext:^(id  _Nullable x) {
    NSLog(@"Next: %@", x);
  }];
}];

One great benefit of using RACCommand this way is that it automatically disables the button while the command is running, so if your signInSignal and userInfoSignal take some time, the button will be automatically disabled in the meantime so that the user can not start the action a second time while its already running.

Upvotes: 1

Related Questions