Chaining dependent signals in ReactiveCocoa

Sergey Mikhanov picture Sergey Mikhanov · Apr 3, 2013 · Viewed 8.2k times · Source

In ReactiveCocoa, if we chaining several dependent signals, we must use subscribeNext: for the next signal in the chain to receive the value previous signal produced (for example, a result of an asynchronous operation). So after a while, the code turns into something like this (unnecessary details are omitted):

RACSignal *buttonClickSignal = [self.logIn rac_signalForControlEvents:UIControlEventTouchUpInside];

[buttonClickSignal subscribeNext:^(UIButton *sender) {    // signal from a button click
    // prepare data

    RACSignal *loginSignal = [self logInWithUsername:username password:password];    // signal from the async network operation

    [loginSignal subscribeNext:^void (NSDictionary *json) {
        // do stuff with data received from the first network interaction, prepare some new data

        RACSignal *playlistFetchSignal = [self fetchPlaylistForToken:token];         // another signal from the async network operation

        [playlistFetchSignal subscribeNext:^(NSDictionary *json) {
            // do more stuff with the returned data
        }];

        // etc
    }];
}];

This ever-increasing nesting does not look much better than the non-reactive example that is given in the documentation:

[client logInWithSuccess:^{
    [client loadCachedMessagesWithSuccess:^(NSArray *messages) {
        [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) {
            NSLog(@"Fetched all messages.");
        } failure:^(NSError *error) {
            [self presentError:error];
        }];
    } failure:^(NSError *error) {
        [self presentError:error];
    }];
} failure:^(NSError *error) {
    [self presentError:error];
}];

Am I missing something? Is there a better pattern of chaining dependent work in ReactiveCocoa?

Answer

Justin Spahr-Summers picture Justin Spahr-Summers · Apr 5, 2013

This is when the RACStream and RACSignal operators start really coming in handy. In your particular example, you can use -flattenMap: to incorporate results into new signals:

[[[buttonClickSignal
    flattenMap:^(UIButton *sender) {
        // prepare 'username' and 'password'
        return [self logInWithUsername:username password:password];
    }]
    flattenMap:^(NSDictionary *json) {
        // prepare 'token'
        return [self fetchPlaylistForToken:token];
    }]
    subscribeNext:^(NSDictionary *json) {
        // do stuff with the returned playlist data
    }];

If you don't need the results from any step, you can use -sequenceMany: or -sequenceNext: instead for a similar effect (but for a clearer expression of intent).