Search code examples
iosobjective-creactive-programmingreactive-cocoa

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.


Solution

  • 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];
    }];