ReactiveCocoa and delegates - ios

I'm trying to communicate with the login service and update the UI the reactive way. The thing is that my login service works with delegates and almost every example I find works with blocks.
I wrote a solution that works, but it seems a bit to clunky, I'm not sure if this is the best way:
LoginViewController:
- (void) viewDidLoad
{
[super viewDidLoad];
//Assign the "loginCommand" command to the button. It'll get executed on button pressed and the button is only enabled when the command says so.
self.entrarBtn.rac_command = self.viewModel.loginCommand;
//Subscribe and respond to command's successful signals
#weakify(self);
[self.viewModel.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) {
[loginSignal subscribeNext:^(id x) {
#strongify(self);
[self.viewPresenter enterMainNavigation];
}];
}];
//Subscribe and respond to command's error signals
[self.viewModel.loginCommand.errors
subscribeNext:^(NSError* error) {
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:#"ERROR" message:[NSString stringWithFormat:#"Error: %#", error.localizedDescription] delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[alert show];
}];
}
LoginViewModel:
- (id)init
{
self = [super init];
if(self) {
self.loginCommand = [[RACCommand alloc] initWithEnabled:self.enableLoginSignal
signalBlock:^RACSignal *(id input) {
return [self loginSignal];
}];
}
return self;
}
- (RACSignal *)loginSignal
{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//LOGIN OK
RACDisposable* loginOKDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginOK)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
PositionGlobalService *positionGlobalService = [PositionGlobalService sharedInstance];
positionGlobalService.delegate = self;
[positionGlobalService getPositionGlobal];
}];
//GETTING USER INFO DELEGATE THEN SEND THE COMPLETED SIGNAL
RACDisposable* positionOKDisposable = [[self rac_signalForSelector:#selector(positionGlobalServiceDidReceivePositionGlobal)
fromProtocol:#protocol(PositionGlobalServiceDelegate)] subscribeNext:^(id x) {
[subscriber sendNext:nil];
[subscriber sendCompleted];
}];
RACDisposable* positionErrorDisposable = [[self rac_signalForSelector:#selector(positionGlobalServiceDidReceivePositionGlobalError:)
fromProtocol:#protocol(PositionGlobalServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorGettingUserInfo userInfo:nil];
[subscriber sendError:error];
}];
//ERRORS
RACDisposable* loginKODisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginKO)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorKO userInfo:nil];
[subscriber sendError:error];
}];
RACDisposable* deniedDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginKOAccessDenied)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorAccessDenied userInfo:nil];
[subscriber sendError:error];
}];
RACDisposable* connectionErrorDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveConnectionError)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorConnectionError userInfo:nil];
[subscriber sendError:error];
}];
RACDisposable* genericErrorDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveGenericError:)
fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
NSError* error = [NSError errorWithDomain:LoginErrorDomain code:LoginErrorGenericError userInfo:nil];
[subscriber sendError:error];
}];
LoginService *loginService = [LoginService sharedInstance];
loginService.delegate = self;
[loginService checkLogin:self.usuario withPassword:self.password documentType:LoginDocumentTypeNIF saveLogin:YES];
return [RACDisposable disposableWithBlock:^{
[loginOKDisposable dispose];
[positionOKDisposable dispose];
[positionErrorDisposable dispose];
[loginKODisposable dispose];
[deniedDisposable dispose];
[connectionErrorDisposable dispose];
[genericErrorDisposable dispose];
}];
}];
}
As you can see there's a bunch of code that is almost the same for every delegate, that's why I'm unsure whether this is the best way to do it.

Your view looks good, but I have a few suggestions for the model. The main point is that I'd simplify the signals on the LoginService and PositionGlobalService by moving them into the respective classes for those services. You can then merge the errors and create a single signal, e.g.:
#interface LoginService : SomeSuperclass<LoginServiceDelegate>
- (RACSignal *)loginWithID:(NSString *)userid password:(NSString *password);
#end
#implementation LoginService()
- (RACSignal *)loginWithID:(NSString *)userid password:(NSString *)password {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
RACDisposable *errorDisposable = [[RACSignal merge:#[[[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginKO) fromProtocol:#protocol(LoginServiceDelegate)] mapReplace:[NSError errorWithDomain:LoginErrorDomain code:LoginErrorKO userInfo:nil]],
[[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginKOAccessDenied) fromProtocol:#protocol(LoginServiceDelegate)] mapReplace:[NSError errorWithDomain:LoginErrorDomain code:LoginErrorAccessDenied userInfo:nil]],
[[self rac_signalForSelector:#selector(loginServiceDidReceiveConnectionError) fromProtocol:#protocol(LoginServiceDelegate)] mapReplace:[NSError errorWithDomain:LoginErrorDomain code:LoginErrorConnectionError userInfo:nil]],
[[self rac_signalForSelector:#selector(loginServiceDidReceiveGenericError) fromProtocol:#protocol(LoginServiceDelegate)] mapReplace:[NSError errorWithDomain:LoginErrorDomain code:LoginErrorGenericError userInfo:nil]]]] subscribeNext:^(id x) {
[subscriber sendError:x];
}];
RACDisposable *loginDisposable = [[self rac_signalForSelector:#selector(loginServiceDidReceiveLoginOK) fromProtocol:#protocol(LoginServiceDelegate)] subscribeNext:^(id x) {
[subscriber sendNext:x];
[subscriber sendCompleted];
}];
[self checkLogin:userid withPassword:password documentType:LoginDocumentTypeNIF saveLogin:YES];
return [RACDisposable disposableWithBlock:^{
[errorDisposable dispose];
[loginDisposable dispose];
}];
}
}
#end
Then, your login function can become something like this (though I'd probably rename this function since it does two things):
- (RACSignal *)loginSignal
{
return [[[LoginService sharedInstance] loginWithID:self.usuario password:self.password] then:^RACSignal *{
return [[PositionGlobalService sharedInstance] getPositionGlobalSignal];
}];
}];

Related

ReactiveCocoa after catch the error, the button signal not get triggered again

If I add a UIControlEventTouchUpInside signal to a doneButton, and call an API, if the API fails, the catch will be called. But if I try to click the button again, the button control event does not get triggered.
- (void)viewDidLoad {
[super viewDidLoad];
[[[[[self.doneButton rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:^(id x) {
[SVProgressHUD show];
}] flattenMap:^RACStream *(id value) {
return [[HttpService sharedService] updateImageData:UIImageJPEGRepresentation(self.signatureImageView.image, 0.5)];
}] catch:^RACSignal *(NSError *error) {
[SVProgressHUD showErrorWithStatus:error.localizedDescription];
return [RACSignal empty];
}] subscribeNext:^(id x) {
[SVProgressHUD dismiss];
[self.navigationController popToRootViewControllerAnimated:YES];
}];
}
I think this thread will help. https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1218
A signal will automatically be unsubscribed to if it fails / errors. You can use - retry, however that will simply keep trying your operation until is doesn't fail, which, if there is a perpetual issue will just loop indefinitely.
Wrapping this condition in a flattenMap will capture the issue without unsubscribing the initial rac_signalForControlEvents observation.
See mdieps comment in the thread above on GitHub, and maybe do something like.
[[[[self.doneButton rac_signalForControlEvents:UIControlEventTouchUpInside] doNext:^(id x) {
[SVProgressHUD show];
}] flattenMap:^RACStream *(id value) {
return [[[HttpService sharedService] updateImageData:UIImageJPEGRepresentation(self.signatureImageView.image, 0.5)]
catch:^RACSignal *(NSError *error) {
[SVProgressHUD showErrorWithStatus:error.localizedDescription];
return [RACSignal empty];
}];
}] subscribeNext:^(id x) {
[SVProgressHUD dismiss];
[self.navigationController popToRootViewControllerAnimated:YES];
}];
I've not actually constructed a test with this code. Just guessing based on what you might have in your HttpService Class.
You can use RACCommand to solve this problem.
RACCommand *doneCommand =
[[RACCommand alloc] initWithSignalBlock:^RACSignal *(NSString *selected) {
return [[[self updateImageSignal]
doCompleted:^{
[SVProgressHUD dismiss];
[self.navigationController popToRootViewControllerAnimated:YES];
}] doError:^(NSError *error) {
[SVProgressHUD showErrorWithStatus:error.localizedDescription];
}];
}];
self.doneButton.rac_command = doneCommand;
Now create RACSignal that send success and error according to your request.
-(RACSignal *)updateImageSignal {
#weakify(self)
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
#strongify(self)
[[HttpService sharedService] updateImageData:UIImageJPEGRepresentation(self.signatureImageView.image, 0.5)
complete:^(BOOL success) {
if(success)
[subscriber sendNext:#(success)];
else
[subscriber sendError:nil];
[subscriber sendCompleted];
}];
return nil;
}];
}
Hope it will help you. And If you have any question then feel free to ask.

Using Reactivecocoa Events call only once

I've this simple signal for signing in user.
-(RACSignal *)signInSignal {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[self.signInService
signInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text
complete:^(BOOL success) {
if(success)
{
[subscriber sendNext:#(success)];
[subscriber sendCompleted];
}
else
[subscriber sendError:nil];
}];
return nil;
}];
}
and for my button
[[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x) {
NSLog(#"In do next");
self.signInButton.enabled = NO;
self.signInFailureText.hidden = YES;
}]
flattenMap:^id(id x) {
NSLog(#"flatten map");
return [self signInSignal];
}]
subscribeNext:^(NSNumber *signedIn) {
NSLog(#"In subscribe");
self.signInButton.enabled = YES;
self.signInFailureText.hidden = 1;
[self performSegueWithIdentifier:#"signInSuccess" sender:self];
} error:^(NSError *error) {
self.signInButton.enabled = YES;
self.signInFailureText.hidden = 0;
}];
It works perfectly until I get an error so I change the password text and press the login button but it does nothing it means it calls only once (the sign in button is enabled)
I came up with an answer using RACCommand
RACCommand *submitCommand =
[[RACCommand alloc] initWithEnabled:signUpActiveSignal signalBlock:^RACSignal *(id input) {
return [[[self signInSignal]
doCompleted:^{
self.signInButton.enabled = YES;
self.signInFailureText.hidden = 1;
[self performSegueWithIdentifier:#"signInSuccess" sender:self];
}] doError:^(NSError *error) {
self.signInButton.enabled = YES;
self.signInFailureText.hidden = 0;
}];
}];
self.signInButton.rac_command = submitCommand;
Try this:
[[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x){
NSLog(#"In do next");
self.signInButton.enabled = NO;
self.signInFailureText.hidden = YES;
}]
flattenMap:^id(id x){
NSLog(#"flatten map");
return [self signInSignal];
}]
subscribeNext:^(NSNumber*signedIn){
NSLog(#"In subscribe");
self.signInButton.enabled =YES;
BOOL success =[signedIn boolValue];
self.signInFailureText.hidden = success;
if(success){
[self performSegueWithIdentifier:#"signInSuccess" sender:self];
}
}];
- (RACSignal *)signInSignal {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber){
[self.signInService
signInWithUsername:self.usernameTextField.text
password:self.passwordTextField.text
complete:^(BOOL success){
[subscriber sendNext:#(success)];
[subscriber sendCompleted];
}];
return nil;
}];
}

How to return a RACSignal without using [RACSignal createSignal]

For example, My current implementation is like below:
- (RACSignal *)getPlaylist {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[[[buttonClickSignal
flattenMap:^(UIButton *sender) {
return [self logInWithUsername:username password:password];
}]
flattenMap:^(NSDictionary *json) {
return [self fetchPlaylistForToken:token];
}]
subscribeNext:^(NSDictionary *json) {
[subscriber sendNext:json];
[subscriber sendCompleted];
}];
return nil;
}];
}
How to return a new signal without using [RACSignal createSignal] method?
Why don't you just return the mapped buttonClickSignal?
I don't see any problems with just this:
- (RACSignal *)getPlaylist {
return [[buttonClickSignal
flattenMap:^(UIButton *sender) {
return [self logInWithUsername:username password:password];
}]
flattenMap:^(NSDictionary *json) {
return [self fetchPlaylistForToken:token];
}];
}
Since you appear to be ignoring errors right now your current implementation will never actually complete if any of the flattenMapped signals error.

Ask for retry upon a failure of the network API call by using ReactiveCocoa

I am using ReactiveCocoa in my iOS app for the network API requests. What if I want to show an UIAlertView and ask for user to click on the retry button and a retry on the same API call only happens when user click on the retry button, how is that supposed to do?
- (RACSignal*) fetchImportantData {
return [RACSignal createSignal: ^RACDisposable*(id<RACSubscriber> subscriber) {
return [apiCall subscribeNext:^(id x) {
[subscriber sendNext:x];
[subscriber sendCompleted];
} error:^(NSError *error) {
[subscriber sendError:error];
}];
}];
}
This should do the trick.
RACSignal * catchSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
UIAlertView * alertView = [[UIAlertView alloc]
initWithTitle:#"Try again"
message:#""
delegate:nil
cancelButtonTitle:#"No"
otherButtonTitles:#"Yes", nil];
[alertView.rac_buttonClickedSignal subscribeNext:^(NSNumber * buttonIndex) {
if (buttonIndex.integerValue != alertView.cancelButtonIndex)
{
[subscriber sendCompleted];
}
else
{
[subscriber sendError:nil];
}
}];
[alertView show];
return nil;
}];
[[[[[self fetchImportantData] catchTo:catchSignal] repeat] take:1] subscribeNext:^(id x) {
NSLog(#"NEXT: %#", x);
} error:^(NSError *error) {
NSLog(#"ERROR: %#", error);
} completed:^{
NSLog(#"COMPLETED");
}];
So what's happening here is the error from fetchImportantData is being caught by catchTo:, and the signal is then replaced by whatever is sent by that signal (it's kinda like flattenMap:, but for errors). Since we now have control, we can wire up sendCompleted to the "Yes" button and use repeat to have the signal repeat upon completion, while wiring up sendError: to the "No" button so that we can have all subscription stop immediately if the user doesn't want to retry.
When fetchImportantData finally returns a non-error, it'll be sent through and completely skip our catchTo: block, and the signal will complete thanks to our take:1.

How to parallel AFNetworking request and process response in sequence with Reactive Cocoa

I'm trying to fetch JSON data from 5 different URLs. The network requests can be performed in parallel, though the responses have to be processed in a certain order. In addition, I also want to have a single point of error handling logic.
The code I'm having right now is like the following. The problem is, only the subscription of signalFive and signalSix has been invoked. The subscribeNext block for all the other signals has never been invoked. I suspect the problem is because the subscription happens after the sendNext occurs.
Is there a better/standard way to perform this kind of request?
- (RACSubject *)signalForFetchingFromRemotePath:(NSString *)remotePath
{
RACSubject *signal = [RACSubject subject];
[self.requestOperationManager GET:remotePath parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
for (id obj in responseObject) {
[signal sendNext:obj];
}
[signal sendCompleted];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[signal sendError:error];
}];
return signal;
}
FMDatabase *db = [SomeDatabase defaultDatabase];
[db beginTransaction];
RACSubject *singalOne = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_ONE_PATH]];
RACSubject *singalTwo = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_TWO_PATH]];
RACSubject *singalThree = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_THREE_PATH]];
RACSubject *singalFour = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_FOUR_PATH]];
RACSubject *singalFive = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_FIVE_PATH]];
RACSubject *singalSix = [self signalForFetchingFromRemotePath:[self urlStringWithPath:SYNC_SIX_PATH]];
RACSignal *combined = [RACSignal merge:#[singalOne, singalTwo, singalThree, singalFour, singalFive, singalSix]];
[combined subscribeError:^(NSError *error){
[db rollback];
}];
[singalFive subscribeNext:^(NSDictionary *dict) {
[ClassE save:dict];
} completed:^{
[singalSix subscribeNext:^(NSDictionary *dict) {
[ClassF save:dict];
} completed:^{
[singalOne subscribeNext:^(NSDictionary *dict){
[ClassA save:dict];
} completed:^{
[singalTwo subscribeNext:^(NSDictionary *dict){
[ClassB save:dict];
} completed:^{
[singalThree subscribeNext:^(NSDictionary *dict) {
[ClassC save:dict];
} completed:^{
[singalFour subscribeNext:^(NSDictionary *dict){
[ClassD save:dict];
} completed:^{
NSLog(#"Completed");
[db commit];
}];
}];
}];
}];
}];
}];
If you need to enforce a specific order, use +concat: instead of +merge:.
On its own, concatenation means that the requests will not be performed in parallel. If you want to recover that behavior, you can use -replay on each signal (to start it immediately) before passing it to +concat:.
As an aside, nested subscriptions are almost always an anti-pattern. There's usually a built-in operator to do what you want instead.
I usually use combineLatest:
NSArray *signals = #[singalOne, singalTwo, singalThree, singalFour, singalFive, singalSix];
[[RACSignal combineLatest:signals] subscribeNext:^(RACTuple *values) {
// All your values are here
} error:^(NSError *error) {
// error
}];

Resources