Search code examples
ioscocoareactive-cocoa

Is this implementation of a timeout in Reactive Cocoa correct?


I did a login which is connected to a button in ReactiveCocoa. Even I tested this piece of code and it seems to work correctly, I am not sure if I do it right. The login signal returns "next" on success and "error" in any other case. Since I don't want the Button to be unsubscribed on an error I use the catch function.

What I want: I want a timeout to fire after 2 seconds, if the loginSignal is not fired. Is this correctly done? Is it also done right the "reactive way"?

[[[[self.loginButton rac_signalForControlEvents:UIControlEventTouchUpInside]
        doNext:^(id x) {
            [self disableUI];
        }]
        flattenMap:^(id value) {
            return [[[[[self.login loginSignalWithUsername:self.usernameTextField.text
                                               andPassword:self.passwordTextField.text]
                    catch:^RACSignal *(NSError *error) {
                        [self enableUI];
                        [self showAlertWithTitle:NSLocalizedString(@"ERROR_TITLE", @"Error")
                                         message:NSLocalizedString(@"LOGIN_FAILURE", @"Login not successful.")];
                        return [RACSignal empty];
                    }]

                    deliverOn:[RACScheduler mainThreadScheduler]]
                    timeout:2.0 onScheduler:[RACScheduler mainThreadScheduler]]
                    catch:^RACSignal *(NSError *error) {
                        [self enableUI];
                        [self showAlertWithTitle:NSLocalizedString(@"TIMEOUT_TITLE", @"Timeout occured")
                                         message:NSLocalizedString(@"REQUEST_NOT_POSSIBLE", @"Server request failed")];
                        return [RACSignal empty];
                    }];
        }]
        subscribeNext:^(id x) {
            [self enableUI];
            // Go to next page after login
        }];

Solution

  • You should in my opinion use RACCommand which is a nice hub for binding signals to UI :

    RACCommand* command = [[RACCommand alloc] initWithSignalBlock:^(id _) {
        return [[[self.login loginSignalWithUsername:self.username password:self.password] 
                doCompleted:^{
                    // move to next screen
                }]
                timeout:2.0 onScheduler:[RACScheduler mainThreadScheduler]];
    }];
    self.button.rac_command = command;
    

    You can then handle any errors (login or timeout) using the command "errors" signal :

    [[command errors] subscribeNext:^(NSError* err) {
        // display error "err" to the user
    }];
    

    The signal will automatically disable the button while it is executing. If you need to disable other parts of your UI you can use the "executing" signal of the command.

    [[command executing] subscribeNext:^(NSNumber* executing) {
        if([executing boolValue]) {
            [self disableUI];
        } else {
            [self enableUI];
        }
    }];
    
    // bonus note: if your enableUI method took a BOOL you could lift it in one line :
    [self rac_liftSelector:@selector(enableUI:) withSignals:[command executing], nil];
    

    here is a blog article talking about RACCommands