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).
Related
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];
}];
}];
}
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:.
I'm new to ReactiveCocoa world and after reading best practices of ReactiveCocoa here I knew that I need to "avoid explicit subscriptions and disposal" but in all tutorials about network and ReactiveCocoa I saw the same pattern : create signal (make GET or POST request to server, parse result, sendNext, sendCompleted) -> subcsribeNext (do UI stuff or something other with the result) -> subscribeError. So as we see there is an explicit subscription here, which is not good, I think.
Are there some more correct and conceptually pure ways of doing this common thing? rac_liftSelector:withSignals: or something like this? Or when we deal with network calls and AFNetworking we should always use this standard subscription pattern? Detailed explanation will be very helpful.
EDITS:
In my application I have mainly fetching calls, some of them are dependent and others are single (vast majority) like login or fetchWhatever, or postWhatever. All API calls I construct with the same pattern like this (self - is my API manager NVMAPI class which is AFHTTPSessionManager subclass):
-(RACSignal*)loginUserWithEmail:(NSString *)email andPassword:(NSString *)password
{
__block NSURLSessionDataTask* task;
return [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
task = [self GET:kUserLoginEndpoint
parameters:#{#"email": email, #"password": password}
success:^(NSURLSessionDataTask *task, id responseObject) {
NVMUser* user = [[NVMUser alloc] initWithDictionary:responseObject[#"user"]];
[subscriber sendNext:user];
[subscriber sendCompleted];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[subscriber sendError:error];
}];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}] replayLazily];
}
I don't use MVVM and use simple MVC. Here how I make API calls and create signals in view controllers:
[SVProgressHUD showWithStatus:#"Processing..." maskType:SVProgressHUDMaskTypeBlack];
[[[NVMAPI api] loginUserWithEmail:self.emailTextField.text
andPassword:self.passwordTextField.text]
subscribeNext:^(id x) {
[self.activeUser setupWithUser:x];
[SVProgressHUD dismiss];
[self performSegueWithIdentifier:kLoginSeque sender:self];
}
error:^(NSError *error) {
[SVProgressHUD dismiss];
[self showAlertWithText:error.localizedDescription title:#"Error"];
}];
All signals I use the same way like: fetchComments, postStatus, etc. If I have dependent calls I use flattenMap. So I'm interested - is it a right approach for creating signals and for using them (simple subscribeNext)? Or this can be achieved with some more correct and elegant way?
EDIT 2:
Main problem with subscriptions which I see - I don't know how to implement pagination for tableview with them. I have fetchComment method with load paginated comments - each page contain 15 comments. I can't use neither subcribeNext nor RAC() binding for this, right?How can I manage this pattern?
Network calls aren't special, you can compose signals that issue network requests the same way you would compose any other signal.
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.
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");
}];
}];