Send error from doNext block?

183 views Asked by At

I'm using ReactiveCocoa and Overcoat/Mantle/AFNetworking to fetch data and authenticate a user from a RESTful API.

This is the code in the login view controller that manages the login button and textfields for credentials:

@weakify(self);
self.loginButton.rac_command =
[[RACCommand alloc] initWithEnabled:validCredentials
                        signalBlock:^RACSignal *(id input) {
                            @strongify(self);
                            return [[PFUserManager sharedManager] logInUser:self.usernameTextField.text
                                                            password:self.passwordTextField.text];
                        }];

// Handle errors for the login command
[self.loginButton.rac_command.errors subscribeNext:^(NSError *error) {
    // Present the error message
    [PFErrorAlertFactory showOVCError:error];
}];

// Take care of the signal from the request
[[self.loginButton.rac_command.executionSignals flatten] subscribeNext:^(NSNumber *success) {
    @strongify(self);
    [self clearTextFields];
    [self.flowController controllerForMainScreen]; // Transition to "logged in state"
} error:^(NSError *error) {
    @strongify(self);
    [self clearTextFields];
}];

I have this method on a singleton UserManager class:

- (RACSignal *)logInUser:(NSString *)username password:(NSString *)password {
    // Return a cold signal that sends next and complete when user is authenticated and error if authentication failed.

    PFAPIClient *client = [[PFAPIClient alloc] initWithUsername:username password:password];

    @weakify(self);
    RACSignal *loginSignal = [[client rac_POST:kAuthenticationResourcePath parameters:nil] doNext:^(OVCResponse *response) {
        @strongify(self);
        self.currentUser = response.result;
        NSError *error;
        [SSKeychain setPassword:password forService:kKeychainServiceKey account:self.currentUser.username error:&error];
        if (error) {
            [PFErrorAlertFactory showLocalizedDescriptionOfError:error];
        }
    }];

    return loginSignal;
}

This is all good, using this signal as a RACCommand signal for the button. I handle next, error and completed events in the login view controller and it works fine.

As you see in the UserManager code, in the doNext block, I show an error if the Keychain method returns one. I'm a little uncertain as to if this error handling belongs in this class.

It does work, the error shows as an UIAlertView, but should this UserManager class really be responsible for showing the error?

Errors coming from the rac_POST signal is handled by the login view controller, and I would like to handle the error from the Keychain method here as well. Is it possible to send an error to the subscriber of the rac_POST signal from within the doNext block? I'm missing a pointer to the subscriber though... As well, if an error occurs in the Keychain method, the signal still sends next and complete and the login is a success as far as the calling view controller knows. This is clearly not the way it's supposed to work.

Is there any other preferred way of handling this whole situation? I know that side effects in doNext blocks aren't preferred, but in this case I see no other solution as I want the UserManager to own this method and be able to set its own currentUser. Should I wrap this in a new signal and explicitly send next, complete and error instead?

Regards, Jens

1

There are 1 answers

0
Jakub Vano On BEST ANSWER

You can create SSKeychain category:

@interface SSKeychain (RACExtension)
- (RACSignal*)rac_setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
@end

@implementation SSKeychain (RACExtension)

- (RACSignal*)rac_setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account 
{
    [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSError *error;
        BOOL result = [SSKeychain setPassword:password forService:service account:account error:&error];
        if (result) {
            [subscriber sendNext:@(result)];
            [subscriber sendCompleted];
        } else {
            [subscriber sendError:error];
        }
    }];
}

@end

And then use it your UserManager:

- (RACSignal *)logInUser:(NSString *)username password:(NSString *)password 
{
    PFAPIClient *client = [[PFAPIClient alloc] initWithUsername:username password:password];

    @weakify(self);
    return [[[client rac_POST:kAuthenticationResourcePath parameters:nil]
             flattenMap:^RACStream *(OVCResponse *response){
                 @strongify(self)
                 return [[SSKeychain rac_setPassword:password forService:kKeychainServiceKey account:self.currentUser.username]
                         mapReplace:response];
             }]
             doNext:^(OVCResponse *response){
                 @strongify(self)
                 self.currentUser = response.result;
             }]
}