3

Fairly new to ReactiveCocoa, I'm trying to build a signal that asynchronously fetches some resource from a remote API to which the client has to authenticate first. Authentication is handled by first getting a token from the API, and then passing it via some custom HTTP header for each subsequent request. However, the custom header might be set after the fetchResource signal is subscribed to, which in the current situation leads to an unauthenticated request. I guess I could actually build the request in the subscribeNext block of self.authenticationStatus, thus ensuring that the token will be set, but how could I handle the disposition of the signal then?

- (RACSignal *)fetchResource
{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSURLRequest *request = [self.requestSerializer
                                 requestWithMethod:@"GET"
                                 URLString:[[NSURL URLWithString:@"resource" relativeToURL:self.baseURL] absoluteString]
                                 parameters:nil error:nil];
        NSURLSessionDataTask *task = [self dataTaskWithRequest:request
                                      completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {
            if (error) {
                [subscriber sendError:error];
            } else {
                [subscriber sendNext:responseObject];
                [subscriber sendCompleted];
            }
        }];

        // Actually trigger the request only once the authentication token has been fetched.
        [[self.authenticationStatus ignore:@NO] subscribeNext:^(id _) {
            [task resume];
        }];

        return [RACDisposable disposableWithBlock:^{
            [task cancel];
        }];
    }];
}
beauby
  • 550
  • 3
  • 11
  • Is there some reason why you're not splitting up your work into separate signals, and then sequencing them using something like `-concat`, `-then:`, or `-flattenMap:`? That would let you "hold off" on making the resource request until the token has been retrieved. (I might be misunderstanding your situation, though.) – erikprice Aug 25 '14 at 15:48
  • Well, since the `self.authenticationStatus` signal is basically a `RACObserve distinctUntilChanged`, it never really completes, so `-then:` is out. I'm not too sure about how `-concat` works, but I would guess it also awaits for completion. Maybe the solution is to artificially make it complete using `-take:1` and `-then`. However, I would also be interested in a solution that allows for updates each time the `self.authenticationStatus` signal flips back to `YES`. – beauby Aug 26 '14 at 03:45
  • "the custom header might be set after the fetchResource signal is subscribed to" - how do you propose setting a property on an HTTP header that has already been sent? – ColinE Aug 26 '14 at 05:36
  • @ColinE: When I say "setting the custom header", I actually mean calling `-setValue:forHTTPHeaderField:` on the `requestSerializer`, which always happens _before_ the property `RACOberve`d by `self.authenticationStatus` is set to `YES`, so before the request is actually fired. However, in the snippet that I posted, the request might be _forged_ before that happens, leading to an unauthorized request. Does that make it clearer? – beauby Aug 26 '14 at 12:05
  • @beauby Forget how the `self.authenticationStatus` is currently implemented (e.g., `RACObserve` or otherwise) – take a step back and design a graph of inputs and outputs over time. Again, I may be missing something, but it seems like you want to do something like "try to fetch the resource, but if it fails for authentication reasons, then try to get an authentication token, and then fetch the resource again". Is that right? – erikprice Aug 26 '14 at 13:34
  • @erikprice: Not exactly. Basically I want to achieve the following: I have a ViewController that modally presents a sign in view if needed, which is dismissed once the user sent their credentials and got a token back. This is when the header gets set on the `requestSerializer`, and this is when I would like the `fetchResource` signal to start getting data from the server. – beauby Aug 26 '14 at 18:06

1 Answers1

2
- (RACSignal *)fetchTokenWithCredentials:(Credentials *)credentials
{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        // Fetch the token and send it to `subscriber`.
        Token *t = ... ;
        [subscriber sendNext:t];

        return nil;

    }];
}

- (RACSignal *)fetchResourceWithToken:(Token *)token
{
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

        // Use `token` to set the request header. Then fetch
        // the resource and send it to `subscriber`. Basically
        // this part is what you already have.
        Resource *r = ... ;
        [subscriber sendNext:r];

        return nil;

    }];
}

In your view controller, present the modal authentication dialog if you don't have a valid token. When the user taps the "submit" button, do something like the following:

- (IBAction)handleAuthenticationSubmit:(id)sender
{
    Credentials *c = ... ;
    RACSignal *resourceSignal = [[[self fetchTokenWithCredentials:c]
            flattenMap:^(Token *t) {

                return [self fetchResourceWithToken:t];

            }]
            deliverOn:RACScheduler.mainThreadScheduler];

    [self rac_liftSelector:@selector(receiveResource:) withSignals:resourceSignal, nil];
}

- (void)receiveResource:(Resource *)resource
{
    [self.delegate authenticationController:self didReceiveResource:resource];
}
erikprice
  • 6,240
  • 3
  • 30
  • 40