Search code examples
objective-creactive-cocoa

Retry with alert using Reactive Cocoa / Inject side effect before retry


I have just started to look at Reactive Cocoa and thought that a scenario from many of the apps I currently work with should be a good starting point. When the app starts it will check if the user is currently in a white listed country before letting the user in to the app. So the perfect sequence should go something like this:

  1. Check the device location using Core Location.
  2. Reverse code the location using CLGeocoder.
  3. Compare the country code against the white list.
  4. Continue loading the app.

If (1) fails I want to inform the user that location services have to be enabled using a UIAlertView with a single Retry button. And this is where I can't seem to get the hang of how this is supposed to be done. The current code below:

@weakify( self )
[[[[[[self
    findLocation]
    doError:^( NSError *error ) {
        @strongify( self )
        // There was an error fetching the location so we ask
        // the user to enable location services.
        //
        [self askUserToEnableLocationServices];
    }]
    retry] // What do we really retry here? It doesn't seem like it's [self findLocation]
    flattenMap:^RACStream *( CLLocation *newLocation ) {
        @strongify( self )
        return [self reverseGeocodeLocation:newLocation];
    }]
    flattenMap:^RACStream *( NSString *ISOCountryCode ) {
        @strongify( self )
        return [self checkCountryPermissibleSignal:ISOCountryCode];
    }]
    subscribeNext:^( id x ) {
        NSLog( @"Country is valid!" );
    } error:^( NSError *error ) {
        NSLog( @"Error: %@", error );
    }];

Does anyone have any input on this? My guess it that I'm wrong about how retry works. I also suspect that I'm a bit naive on how the whole flow should look. But I have been stuck on this for a couple of evenings now. There is something that hasn't yet clicked.


Solution

  • Just noticed you asked this on SO. I posted a reply on the GitHub repo, but for posterity I'll post my answer here too:

    @weakify( self )
    [[[[[[self
        findLocation]
        doError:^( NSError *error ) {
            @strongify( self )
            // There was an error fetching the location so we ask
            // the user to enable location services.
            //
            [self askUserToEnableLocationServices];
        }]
        retry] // What do we really retry here? It doesn't seem like it's [self findLocation]
    

    -retry doesn't call [self findLocation] again. Instead it creates a new subscription to the RACSignal object that was returned from [self findLocation]. The original subscription is disposed, due to having delivered an error per the contract/semantics of how signals operate when an error is sent.

    Note that the code in your call to -doError: will execute concurrently with the new subscription, which may not be what you intended. In other words, while you're showing an alert view to the user, you're simultaneously resubscribing to the signal returned from -findLocation before the user has had a chance to take any action.

    You can use -catchTo: to specify an alternative signal that should be subscribed to if an error is delivered on the original signal:

    @weakify( self )
    RACSignal *recover = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
        @strongify( self );
        [self askTheUserToEnableLocationServices]
        return nil;
    }];
    
    [[[[[self
        findLocation]
        catchTo:recover]
        flattenMap:^RACStream *( CLLocation *newLocation ) {
            @strongify( self )
            return [self reverseGeocodeLocation:newLocation];
        }]
        flattenMap:^RACStream *( NSString *ISOCountryCode ) {
            @strongify( self )
            return [self checkCountryPermissibleSignal:ISOCountryCode];
        }]
        subscribeNext:^( id x ) {
            NSLog( @"Country is valid!" );
        } error:^( NSError *error ) {
            NSLog( @"Error: %@", error );
        }];