Retrying commands with ReactiveCocoa - ios

I'm trying to refactor my iOS app into ReactiveCocoa and ReactiveViewModel and I'm struggling with trying to work out a couple of best practices.
I'll boil this down to a simple use case - I was to push a view controller which loads some data and shoves it into a table view. If the endpoint call fails for whatever reason, I want to display a view on screen with a retry button.
I currently have this working but it looks a bit filthy. I feel like there must be a better way - am I even doing this correctly?
In my ViewModel's init method, I'm creating my command, which is called as soon as the ViewModel becomes active.
// create the command to load the data
#weakify(self);
self.loadStationsCommand = [[RACCommand alloc] initWithSignalBlock:^(RACSignal *(id input) {
#strongify(self);
return [RACSignal createSignal:^(RACDisposable *(id<RACSubscriber subscriber) {
// load data from my API endpoint
...
BOOL succeeded = ...;
if (succeeded) {
[subscriber sendNext:nil];
[subscriber sendCompleted:nil];
} else {
// failed
[subscriber sendError:nil];
}
return nil;
}
}];
// start the command when the ViewModel's ready
[self.didBecomeActiveSignal subscribeNext:^(id x) {
#strongify(self);
[self.loadStationsCommand execute:nil];
}];
In my UIViewController, I'm subscribing to the command via -
[self.viewModel.loadStationsCommand.executionSignals subscribeNext:^(RACSignal *loadStationsSignal) {
[loadStationsSignal subscribeNext:^(id x) {
// great, we got the data, reload the table view.
#strongify(self);
[self.tableView reloadData];
} error:^(NSError *error) {
// THIS NEVER GETS CALLED?!
}];
}];
[self.viewModel.loadStationsCommand.errors subscribeNext:^(id x) {
// I actually get my error here.
// Show view/popup to retry the endpoint.
// I can do this via [self.viewModel.loadStationsCommand execute:nil]; which seems a bit dirty too.
}];
I must have some misunderstanding as to how RACCommand works, or at the very least I feel I'm not doing this as cleanly as I can.
Why doesn't the error block on my loadStationsSignal get called? Why do I need to subscribe to executionCommand.errors instead?
Is there a better way?

It is a correct way to handle errors with RACCommand. As you can read in the docs, errors of the inner signal are not sent when using executionSignals:
Errors will be automatically caught upon the inner signals, and sent upon
`errors` instead. If you _want_ to receive inner errors, use -execute: or
-[RACSignal materialize].
You can also use RAC additions to UIButton and bind self.viewModel.loadStationsCommand to rac_command of the retry button.
There is a good article which explains RACCommand and shows some interesting patterns to be used with it.

Related

One RACSignal used at one time

I have a test example with one button. When user presses the button is called RAC_request and request is executing. If user presses this button a many times for a short time, many requests will be execute asynchronously. I want to create logic to previously signal cancelled when new request is executing by Reactive Cocoa. I know that exists switchToLatest in Reactive Cocoa, but I can't do that logic work correctly. How do this by RAC?
if user presses the button a many times for a short time, you can use throttle to set the time. if a interval times have many nextValue, it only take newest. also you can use switchToLatest. There is a easy example, i hole that would be useful for you.
[button.rac_command execute:nil];
button.rac_command = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:#"TestSignal"];
[subscriber sendCompleted];
return [RACDisposable disposableWithBlock:^{
}];
}];
}];
[[[button.rac_command.executionSignals throttle:0.5] switchToLatest]subscribeNext:^(id x) {
NSLog(#"%#", x);
}];

ReactiveCocoa Combine Latest with Button press and Text Field Delegate Signal

I've created a signal to wrap the UITextField Delegate Method textFieldShouldReturn:.
- (RACSignal *)textFieldReturnPressed
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[self rac_signalForSelector:#selector(textFieldShouldReturn:)
fromProtocol:#protocol(UITextFieldDelegate)]
subscribeNext:^(RACTuple *tuple) {
[subscriber sendNext:tuple.first];
}];
return nil;
}];
}
In viewDidLoad, I'm attempting to subscribe to the combination of this signal and a button press. In effect, I'd like a user to be able to tap a button and do some things (login), or press return on the keyboard and do the same things (login).
I've created the following signal to combine the two signals:
RACSignal *loginSignal = [RACSignal
combineLatest:#[[loginButton
rac_signalForControlEvents:UIControlEventTouchUpInside],
[self textFieldReturnPressed]]];
I then subscribe to this event like so:
[loginSignal
subscribeNext:^(id x) {
NSLog(#"%#", x);
}];
When I press the return key on the keyboard, the log above doesn't print (although I have verified sendNext is called from the signal). However, when I trigger the login button signal, the log prints ie. combineLatest passes through the signal.
I've experimented with adding startWith:nil to the loginButton signal, because, as I've found in other posts/Github issues, CombineLatest requires each signal to have been sent at least once, but my stream executes immediately.
I'm sure I could somehow filter at that point to prevent the stream from executing, but that feels a bit hacky. Any recommendations?
The reason why combineLatest isn't give the desired effect is that it needs every signal passed to send at least one next event for subscribers to start receiving next events.
To achieve the effect you want, i.e. two signals with each passing a next event irrespective of the other's state, you should merge: the signals.
Example:
RACSignal *loginSignal = [[loginButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
merge:[self textFieldReturnPressed]];

RACSignal doNext with signals

I need to be able to use doNext with a signal inside, not just a block. The problem is that I use signals for side effects too.
When I do this kind of things my code looks really messy.
For example:
[httpRequestSignal flattenMap:^(Response *response) {
// Save the response.
return [[self saveResponse:response] then:^{
// Return the response.
return [RACSignal return:response];
}];
}];
I'm tempted to use first and make the code look much better, but it looks wrong making a signal synchronous.
[httpRequestSignal doNext:^(Response *response) {
// Save the response.
[[self saveResponse:response] first];
}];
Any advice?

ReactiveCocoa - Changing side effects into signals

In my application I have a signal which triggers some asynchronous network activity via flattenMap. I want to display a loading indicator while the network activity is in progress.
My current solution works just fine:
[[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x) {
// show the loading indicator as a side-effect
self.loadingIndicator.hidden = NO;
}]
flattenMap:^id(id x) {
return [self doSomethingAsync];
}]
subscribeNext:^(NSNumber *result) {
// hide the indicator again
self.loadingIndicator.hidden = YES;
// do something with the results
}];
This works, however I would like to change the above code so that the hidden property of the loading indicator can be set via a signal.
Is this possible?
Elsewhere in my app I have more complex requirements where the visibility of an element depends on a few different 'events', being able to compose these via signals would be much better.
RACCommand is tailor-built for exactly this use case, and will usually result in dramatically simpler code than the alternatives:
#weakify(self);
RACCommand *signInCommand = [[RACCommand alloc] initWithSignalBlock:^(id _) {
#strongify(self);
return [self doSomethingAsync];
}];
self.signInButton.rac_command = signInCommand;
// Show the loading indicator while signing in.
RAC(self.loadingIndicator, hidden) = [signInCommand.executing not];
It looks like your signal is: when signInButtonSignal or resultSignal send a value, invert the last value of hidden. That's easy enough.
[[[hiddenSig replayLast] not] sample:[RACSignal merge:#[signInButtonSignal, resultSignal]];
I'm using exactly that construct for a situation similar to yours. It might be nice to wrap it up into an operator:
- (RACSignal *)toggle:(RACSignal *)toggler
{
return [[[self replayLast] not] sample:toggler];
}
Then you have just
[hiddenSig toggle:[RACSignal merge:#[signInButtonSignal, resultSignal]]];
Another possibility might be a class method, tying the state to a mapping Block:
+ (RACSignal *)toggle:(RACSignal *)toggler initially:(BOOL)initial
{
__block BOOL currVal = initial;
return [[toggler map:^id (id _) {
currVal = !currVal;
return #(currVal);
}] startWith:#(initial)];
}
and then
[RACSignal toggle:[RACSignal merge:#[signInButtonSignal, resultSignal]]
initially:NO];
The answer from Josh helped quite a bit, but in the end I found a simpler solution. Simply breaking the pipeline into two signals, one for the button press, the other for the subsequent asynchronous activity. I then merged the two to give a signal which I used to bind to the loadingIndicator's hidden property:
// a signal that triggers sign-in
RACSignal *signInStartSignal = [self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside];
// a signal that emits the sign in result
RACSignal *signInResultSignal =
[signInStartSignal
flattenMap:^id(id x) {
return [self doSomethingAsync];
}];
[signInResultSignal
subscribeNext:^(NSNumber *result) {
// do something based on the result
}];
// merge the two signals
RACSignal *signInInProgress =
[[RACSignal merge:#[signInResultSignal, signInStartSignal]]
map:^id(id value) {
// if the signal value is a UIButton, the signal that
// just fired was the signInStartSignal
return #(![[value class] isSubclassOfClass:[UIButton class]]);
}];
RAC(self.signInFailureText,hidden) = signInInProgress;

Why Does RACCommand's block return a signal?

I've been learning a lot about ReactiveCocoa but one thing still puzzles me: why does the signal block on RACCommand return a signal itself?
I understand the use cases of RACCommand, its canExecute signal and signal block, and how it can be hooked up to UI elements. But what case would there be ever for returning something other than [RACSignal empty]?
infoButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
// Do stuff
return [RACSignal empty];
}];
There are exceptions to every rule, but generally you want all your "// Do stuff" to be captured by the returned signal. In other words, your example would be better as:
infoButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id input) {
return [RACSignal defer:^{
// Do stuff
return [RACSignal empty];
}];
}];
The direct benefit of this change is that, for the duration of "// Do stuff", your infoButton will be disabled, preventing it from being clicked/tapped until the returned signal has completed. In your original code, the "do stuff" is outside of the signal, and as such your button won't be disabled properly.
For work that doesn't have much latency, for example making UI changes in response to a button tap, then the enabled/disabled feature of RACCommand doesn't buy you much. But if the work is a network request, or some other potentially long running work (media processing for example), then you definitely want all of that work captured within a signal.
Imagine you have a command that should load list of items from network. You could use side effects in signal block or return a signal that would actually send these items. In the latter case you can do the following:
RAC(self, items) = [loadItems.executionSignals switchToLatest];
Also all errors sent by signal would be redirected to errors signal, so:
[self rac_liftSelector:#selector(displayError:)
withSignals:loadItems.errors, nil];
It's impossible with [RACSignal empty]-powered commands.
I have an example that might be helpful, though others might may be able to explain it better.
RACCommand Example
But basically, the way you have it with returning +empty it seems sort of pointless, as invoking the command will basically be using side effects, which we want to avoid.
Instead of using this:
infoButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id input) {
return [RACSignal defer:^{
// Do stuff
return nil;
}];
}];
You can use this built-in method to control the button state:
- (id)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock;

Resources