1

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.

Valerio Santinelli
  • 1,592
  • 2
  • 27
  • 45
  • possible duplicate of [How can I subscribe to the completion of a command's execution signals without a nested subscription?](http://stackoverflow.com/questions/22366964/how-can-i-subscribe-to-the-completion-of-a-commands-execution-signals-without-a) – Michał Ciuba Mar 12 '15 at 13:05

2 Answers2

1

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];
}];
Ian Henry
  • 22,255
  • 4
  • 50
  • 61
  • I'd have moved the `then` block to the `RACCommand` in the first pace, but all those calls to `ALTAlert` are actually just progress HUD being displayed or dismissed and they belong to the view. I might just bind a property so that I can show/hide the `ALTAlert`. And I might bind the `ALTAlert error` to a side-effect of the whole RACCommand eventually. I'm going to give it a try. – Valerio Santinelli Mar 12 '15 at 16:29
  • I tried going that route but I still need to grab the error from the `RACCommand`. And the problem is that I still need to `subscribeNext` to `errors` which is fine but I'll also receive the `completed` from the `executionSignals`. I suppose I'll stick to the current implementation until a different implementation of `RACCommand` will be available. – Valerio Santinelli Mar 13 '15 at 09:22
0

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).
Rui Peres
  • 25,741
  • 9
  • 87
  • 137
  • I checked that out but it doesn't look like my case. The command is bound to a button that starts the login with Twitter through a `RACCommand`. That RACCommand sends as next a signal. At that point I should `flattenMap` or `subscribeNext` to the returned signal and that's not very elegant. Is there a way to tell a RACCommand to send me its inner signal instead? – Valerio Santinelli Mar 12 '15 at 11:16
  • 1
    Why do you think that `flattenMap` is not very elegant? It's a pretty standard way of chaining signals as far as I know. – Michał Ciuba Mar 12 '15 at 12:40
  • I didn't mean that flattenMap is inelegant. It's the fact that I need to nest on the inner signal that isn't elegant. I checked the dupe you mentioned and it solves my issue but it looks very convoluted. I thought there might have been a method or a macro to unwind this use case. – Valerio Santinelli Mar 12 '15 at 13:38
  • I agree that it looks convoluted. I think it is caused by the complexity of the `RACCommand` itself. It is a quite powerful concept, but in this case with great power comes great complexity. There are plans to deprecate `RACCommand` in ReactiveCocoa 3.0: https://github.com/ReactiveCocoa/ReactiveCocoa/issues/1369 – Michał Ciuba Mar 12 '15 at 14:28