Error block not called while using ReactiveCocoa - ios

For some reason I am not getting the error message to come through. (I've simplified the code here to get straight to the point.)
// Send an error message
_loginButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendError:error]; // Pretend this is a real error
return nil;
}];
}];
// Subscribe to loginButton's returned signal
[_loginButton.rac_command.executionSignals subscribeNext:^(RACSignal *loginSignal) {
[loginSignal subscribeError:^(NSError *error) {
NSLog(#"A");
} completed:^{
NSLog(#"B");
}];
}];
This prints "B". Any idea why? If -sendError: is called on the subscriber, why does the completion block receive it?

As you've discovered, RACCommand automatically catches errors within executionSignals.
This is intended to be a convenience for operators like -flatten, -concat, and -switchToLatest, which would otherwise prematurely terminate if an error occurs on any of the inner signals.
If all you care about is knowing when an error occurs, you should use RACCommand.errors instead. If you want to know where the error originated, checking the error domain and code may be easier (or at least more intuitive) than subscribing to the error event of each inner signal.
Subscriptions-within-subscriptions, and even subscriptions in general, are something of a code smell in RAC. Even if you don't want to use errors, there are generally higher-level operators to accomplish what you want (like using -map: to apply a -catch: to each inner signal).

This seems to work (materialize it and dematerialize it) based on this suggestion.
// Send an error message
_loginButton.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendError:error]; // Pretend this is a real error
return nil;
}] materialize];
}];
// Subscribe to loginButton's returned signal
[_loginButton.rac_command.executionSignals subscribeNext:^(RACSignal *loginSignal) {
[[loginSignal dematerialize] subscribeError:^(NSError *error) {
NSLog(#"A");
} completed:^{
NSLog(#"B");
}];
}];

Related

Reactive Cocoa Legacy - How to handle multiple independent signals in parallel?

I would like to launch multiple independent networking request in parallel, and subscribe to when all requests are done (I don't care if they are completed or error).
For now, I used combineLatest. Thus the requests are made in parallel (as I want), but when a signal sends error, I got an error in the subscription.
I can't find an operator that allows me to launch multiple independent request in parallel, and listen to when all are done.
Sincerely
EDIT
In other cases (where I need to use the signals individually), I need to subscribe to the error. So I still need my signal to be able to sendError:.
My use case is this:
On the one hand, I have an app that can do a full synchronisation. This will launch all independent request. I need to know when all requests are done in order to dismiss the progressHUD. I don't care if my signals have done sendErroror sendCompleted.
But on the other hand, I have multiple buttons, where I can relaunch individually each request, and in this case, I need to tell the user if the signal succeded or failed.
I came up with an answer, using the catch operator:
I add the catch RAC operator to all my signal, that make a new [RACSignal empty]in case the signal fails:
NSArray* allSignals = #[signal1, signal2, signal3, signal4];
NSMutableArray* optionalSignals = [NSMutableArray array];
for (RACSignal* signal in allSignals) {
RACSignal* catchableSignal = [signal catch:^RACSignal *(NSError *error) {
return [RACSignal empty];
}];
[optionalSignals addObject:catchableSignal];
}
return [RACSignal combineLatest:optionalSignals];
This does not change the nature of my individual signals, and I benefit from the combineLatestfeatures.
EDIT
If anyone has a more elegant RAC way to handle the for...in loop, I am still interested.
You can use rac_sequence and map instead of a loop. Also, in your case using catchTo will work in the same way as catch while making code more concise:
NSArray* allSignals = #[signal1, signal2, signal3, signal4];
RACSequence *optionalSignals = [[allSignals rac_sequence] map:^id(RACSignal *signal) {
return [signal catchTo:[RACSignal empty]];
}];
return [RACSignal combineLatest:optionalSignals];
You can try this code:
- (void)getAllItemsWithCompletion:(void (^)())completion {
NSArray *signals = [self.allItems.rac_sequence map:^id(Item *item) {
return [[[self getForItemSignal:item] doCompleted:^{
//done
}] doError:^(NSError *error) {
//Change the button title to retry
}];
}].array;
[[RACSignal combineLatest:signals] subscribeCompleted:^{
NSLog(#"All tasks are done");
}];
}
- (RACSignal *)getForItemSignal:(Item *)item {
return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
[APIClient getFeeForItemUUID:item.UUID
success:^(Fee *fee) {
[subscriber sendNext:#(fee)];
[subscriber sendCompleted];
}
failure:^(NSError *error) {
[subscriber sendError:error];
[subscriber sendCompleted];
}];
}];
}

ReactiveCoca Design Pattern for Forwarding / Chaining Signals

I have the following classes and methods:
Class A
- (RACSignal *)createX
{
NSDictionary *parameters = #{};
return [[[[HTTPClient sharedClient] rac_POST:#"X/" parameters:parameters]
map:^id(OVCResponse *response) {
[self logResponse:response];
return response.result;
}] catch:^RACSignal *(NSError *error) {
return [RACSignal error:[self handleError:error]];
}];
}
Class B
- (void)requestData
{
[[self.myClassA createX]
subscribeNext:^(NSArray *results) {
DDLogDebug(#"response : %#", results);
}
error:^(NSError *error) {
[self.dataManager sendError:error];
}];
}
Class C
- (void)retrieveData
{
[self.myClassB requestData];
}
What is the best way to design requestData in Class B such that the results array can be accessed in Class C.
i.e.
Should I forward the Array some way using [array rac_sequence],
should I create a new signal inside requestData, should requestData return a RACSignal instead of void?
Any help or guidance would be greatly appreciated. Thanks.
I believe you want to use doNext instead of subscribeNext in Class B.
I'm not entirely clear on your use case here but I think you are mixing paradigms. RAC stuff is always asynchronous so in order to access the result of your network request synchronously you will have to store it in some way.
You could bind the result to a property on ClassB or you could use RACCommand, something like:
[[RACCommand alloc] initWithEnabled:RACObserve(self, valid) signalBlock:^RACSignal *(id input) {
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//make network call
//send responseObject to subscriber
[subscriber sendNext:responseObject];
//[subscriber sendError:#NSError#] //send error if something went wrong
[subscriber sendCompleted];
return nil;
}] materialize];
}];
You can then subscribe to the executionSignals of the RACCommand which streams a RACSignal for each execution of the command which you have control over in the block described above.
So I think your options are:
RACCommand pattern, look into it a bit more
Bind result of network call to a property for synchronous access
Return a RACSignal as you describe upon calling the method
Possibly look into replay() or replayLast() here as you could then store a reference to the RACSignal and subscribe to it for access to its last value

ReactiveCocoa, RACCommand show message or execute segue after completed

I'm using RACCommand for my UI button click event. I'm using MVVM architecture. In my ViewModel I have this:
#property (strong, nonatomic) RACCommand *executeRegistration;
Inside "init" I have this:
self.executeRegistration = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input)
{
return [self executeSearchSignal];
}];
Execute search signal is this:
- (RACSignal *)executeSearchSignal {
return [[[self.services insertUserRegistration]
registerUserName:self.userName]
logAll];
}
My "[self.services insertUserRegistration]
registerUserName:self.userName" is this:
#weakify(self);
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
#strongify(self);
Manager *manager = [Manager sharedManager];
manager._delegate = self;
RACSignal *successSignal =
[self rac_signalForSelector:#selector(manager:didSuccesWithoutError:)
fromProtocol:#protocol(ManagerDelegate)];
RACSignal *failSignal =
[self rac_signalForSelector:#selector(manager:didFailWithError:)
fromProtocol:#protocol(ManagerDelegate)];
[[successSignal map:^id(RACTuple *tuple)
{
return tuple.second;
}] subscribeNext:^(id x) {
[subscriber sendNext:x];
[subscriber sendCompleted];
}];
[[failSignal map:^id(RACTuple *tuple)
{
return tuple.second;
}] subscribeNext:^(id x) {
[subscriber sendError:x];
}];
[manager insertUserRegistration:name];
return nil;
}];
My ViewController has this in BindViewModel method:
[self.finishRegistrationButton.rac_command.executionSignals subscribeNext:^(RACSignal *loginSignal) {
// Log a message whenever we log in successfully.
[loginSignal subscribeCompleted:^{
NSLog(#"I'm here.");
}];
}];
self.finishRegistrationButton.rac_command = self.viewModel.executeRegistration;
With my "logAll" atribute I can see everything executing, the problem is it never goes into subsrcibeCompleted after everything is ok. I want to show error message if there is an error or perform segue if everything is ok. What am I doing wrong? Can you please explain how to to that properly? I'm stuck here for quite some time now.
I did it. RACCommand 'does not have subscribeError'. Signals sent does not include error events. There is special property 'errors'. In that property, every signal that sends error sends is as 'next'. so, solution is to use this:
[self.executeRegistration.executionSignals subscribeNext:^(RACSignal *signal){
[signal subscribeCompleted:^{
NSLog(#"Registered");
}];
}];
[self.executeRegistration.errors subscribeNext:^(id x) {
NSLog(#"Error");
}];
This is ok. There is no need to subscribeNext if you don't want every new value. This is actually really cool stuff, but I've read that it is too confusing why it is not sending errors as in classic implementation (it was for me :)). That will be included in 3.0 if I'm not mistaken.
self.finishRegistrationButton.rac_command.executionSignals returns a signal of signals, so you'll want to make use of switchToLatest, like so:
self.finishRegistrationButton.rac_command.executionSignals.switchToLatest
This function intercepts signals and switches to the latest received, sending next, errors and completes from that signal instead of the signal operated on. It's very handy for operations that send signals over time.
This code will only work for executionSignals that hold a single signal, if you want multiple targets then you'll need something a bit more complex. If you ever need to change your code to work that way, you might want to have a look at flattenMap:.

How to chain signals in a proper way with Reactive Cocoa?

I'm using ReactiveCocoa in a new iOS app. I'm new to reactive programming so I'm still trying to understand what's the proper way to chain signals.
Right now I have the following flow for the "login with Twitter" button.
The ALTUserManager class has the following method for managing the whole login phase by calling some functions in a library that presents the Twitter login panel and does all of the OAuth stuff:
- (RACSignal *)loginTwitter:(UIViewController *)vc {
RACSignal *loginSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[ALTTwitter sharedInstance]isLoggedIn:^(BOOL loggedIn) {
if(loggedIn){
[subscriber sendCompleted];
}
else{
[[ALTTwitter sharedInstance]login:vc andSuccess:^{
[subscriber sendCompleted];
} failure:^(NSString *error) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[NSLocalizedDescriptionKey] = error;
[subscriber sendError:[NSError errorWithDomain:#"" code:1 userInfo:userInfo]];
}];
}
}];
return nil;
}];
return loginSignal;
}
I'm using the MVVM pattern so in my ViewModel I've added the following command inside its init method:
self.twitterLoginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[ALTUserManager sharedInstance] loginTwitter:nil];
}];
In my view controller I'm handling the presentation logic where I block the interface while showing a progress hud and eventually report the error or go past the login screen if everything is fine:
self.twBtn.rac_command = self.viewModel.twitterLoginCommand;
[self.viewModel.twitterLoginCommand.executionSignals subscribeNext:^(id x) {
NSLog(#"%#", x);
[x subscribeCompleted:^{
NSLog(#"%#", #"completed");
[ALTAlert wait:#""];
[[self.viewModel appLoginWithTwitter] subscribeNext:^(id x) {
NSLog(#"%#", x);
} error:^(NSError *error) {
[ALTAlert dismiss];
[ALTAlert error:error.localizedDescription];
} completed:^{
[ALTAlert dismiss];
#strongify(self);
[self goToChart];
}];
}];
}];
[self.viewModel.twitterLoginCommand.errors subscribeNext:^(NSError *error) {
NSLog(#"Login error: %#", error);
[ALTAlert dismiss];
[ALTAlert error:error.localizedDescription];
}];
I'm pretty sure this could be rewritten in a better way. My concern is mainly about that [x subscribeCompleted] line. What would be the correct approach?
Thanks!
UPDATE
I tried moving all the logic to the ViewModel inside the RACCommand but I still need to catch the errors happening inside the RACCommand.
Subscribing to the errors signal isn't an option as the RACCommand would still return the completed event as well thus making my presentation logic unable to tell if everything went fine or not.
I haven't tried setting a BOOL inside the RACCommand with a side-effect in case of errors and observe it in the view. But that approach seems a bit hacky anyway.
You can simplify the nesting a bit by using the then helper, which will simplify error handling and prevent the separate twitterLoginCommand.errors subscription:
[self.viewModel.twitterLoginCommand.executionSignals subscribeNext:^(id x) {
[x then:^{
NSLog(#"%#", #"completed");
[ALTAlert wait:#""];
return [self.viewModel appLoginWithTwitter];
}] subscribeNext:^(id x) {
NSLog(#"%#", x);
} error:^(NSError *error) {
[ALTAlert dismiss];
[ALTAlert error:error.localizedDescription];
} completed:^{
[ALTAlert dismiss];
#strongify(self);
[self goToChart];
}];
}];
This is a little weird, though. Because you can get into weird states if twitterLoginCommand fires again before the appLoginWithTwitter signal completes. This might not be possible given the rest of the app, but just looking at this block of code in isolation it's something that would concern me.
A better thing to do might be to move that then block into the RACCommand, to ensure that that will never happen (as an RACCommand won't execute again until the previous one finished executing.) Though without seeing more of the code I can't really say if that's a reasonable change.
This is a tricky thing to clean up further because it's inherently side-effectful. If you create a reactive bridge for the ALTAlert class, you could clean up a lot of those subscriptions, as you could just say "look at this signal of signals, and make your state reflect it." Then you can just pass that the execution signals and not have to worry about doing something grosser here.
Then your only real side effect is goToChart, which you can do as something a little simpler:
[[[self.viewModel.twitterLoginCommand.executionSignals flattenMap:^(id x) {
return [x materialize];
}] filter:^(RACEvent *event) {
return event.eventType == RACEventTypeCompleted;
}] subscribeNext:^(id x) {
#strongify(self);
[self goToChart];
}];
Not sure if you have seen the design guidelines, but these show you some solutions on how to avoid the -subscribeNext:error:completed: pattern. Specifically these:
The RAC() or RACChannelTo() macros can be used to bind a signal to a property, instead of performing manual updates when changes occur.
The -rac_liftSelector:withSignals: method can be used to automatically invoke a selector when one or more signals fire.
Operators like -takeUntil: can be used to automatically dispose of a subscription when an event occurs (like a "Cancel" button being pressed in the UI).

Making signal retry only to new subscribers

I am using the typical pattern of a cached-replay signal:
- (RACSignal *)resultSignal {
if (!_resultSignal) {
_resultSignal = [self createResultSignal];
}
return _resultSignal;
}
- (RACSignal *)createResultSignal {
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[someObject doWorkWithCompletionBlock:^(id result) {
[subscriber sendNext:result];
[subscriber sendCompleted];
}
failure:^(NSError *error) {
[subscriber sendError:error];
}];
return nil;
}] replay];
}
This works great, but now I want to add the ability to retry if the work failed. I can use -[RACSignal retry] or -[RACSignal retry:], but this would not notify the subscribers of an error immediately. Instead, what I want is for existing subscribers to this signal to get the error, but subsequent calls to -resultSignal to receive a new signal, or retry the existing one.
I could -catch: this signal and set _resultSignal to nil in the block, but then I'd have to worry about race conditions, and I don't think that'd be the "reactive" way to do this.
What would be the appropriate way of implementing this behavior?
Update:
Thanks to #joshaber! I ended up going with my suggested approach, and I think it's not too bad:
- (RACSignal *)resultSignal {
#synchronized(_resultSignal) {
if (!_resultSignal) {
#weakify(self);
_resultSignal = [[self createResultSignal] catch:^RACSignal *(NSError *error) {
#strongify(self);
self.resultSignal = nil;
return [RACSignal error:error];
}];
}
return _resultSignal;
}
}
- (void)setResultSignal:(RACSignal *)signal {
#synchronized(_resultSignal) {
_resultSignal = signal;
}
}
I've been thinking about this for a while. Does it help if I wish I had a great answer? ;)
I could -catch: this signal and set _resultSignal to nil in the block, but then I'd have to worry about race conditions, and I don't think that'd be the "reactive" way to do this.
This is what I'd lean towards.
Alternatively, you could use a signal of signals. It'd send whatever the latest signal is. Existing subscribers would be subscribed to the old signal and new subscribers would get the retried signal. But that'd probably require using a RACSubject manually, so I don't know that the additional complexity is worth it, compared to the other solutions.

Resources