rmp2150
rmp2150

Reputation: 797

Force one method to wait for another to complete

In my original view controller before I segue to the Destination View Controller, I call a method that gets my key parameter and I then set the key parameter in my destination view controller in the following method. However, the key parameter is set before the doSomethingToGetKey method is completed so a null value is passed. I was wondering if there was a way to make the second method wait for the first one to finish.

Origin View Controller:

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{

[self doSomethingToGetKey];
[segue.destinationViewController setKey:_key];
}

-(void)doSomethingToGetKey:(ACAccount *)account{

[_apiObject postReverseOAuthTokenRequest:^(NSString *authenticationHeader) {

    APIObject *api = [APIObject APIOSWithAccount:account];
    [api verifyCredentialsWithSuccessBlock:^(NSString *username) {


        [api postReverseAuthAccessTokenWithAuthenticationHeader:authenticationHeader successBlock:^(NSString *oAuthToken, NSString *oAuthTokenSecret, NSString *userID, NSString *screenName) {

            _key = oAuthToken;
            _secret = oAuthTokenSecret;
            _userId = userID;



        } errorBlock:^(NSError *error) {

            NSLog(@"ERROR 1, %@", [error localizedDescription]);
            exit(1);

        }];

    } errorBlock:^(NSError *error) {

        NSLog(@"ERROR 2: %@",error.description);
        exit(1);

    }];

} errorBlock:^(NSError *error) {

    NSLog(@"ERROR 3: %@",error.description);
    exit(1);

}];

};

Upvotes: 1

Views: 798

Answers (2)

Rob
Rob

Reputation: 437381

Generally the pattern involves employing a completion block, e.g.:

- (void) doSomethingToGetKeyWithCompletionHandler:(void (^)(NSString *key))completion
{
    // perform asynchronous request

    // in its completion handler, call the `completion` block parameter given above

    [api doingSomeOtherAsynchronousMethodWithCompletion:^{
        completion(key);
    }];
}

Then, instead of:

[self doSomethingToGetKey];
[self doSomethingElseWithKey:_key];

You could do:

[self doSomethingToGetKeyWithCompletionHandler:^(NSString *key){
    [self doSomethingElseWithKey:key];
}];

But, in this case, you're trying to do all of this inside prepareForSegue. That changes the problem entirely, because the segue will still be performed before this asynchronous method is called, defeating the purpose of this completion block pattern.

You therefore also want to change your button (or whatever) to not perform the segue itself, but rather call an IBAction, and then have that perform the segue programmatically (as shown here). Thus, you'd end up with an IBAction like:

- (IBAction)tappedButton:(id)sender 
{
    // perhaps update UI so user knows something slow is happening first, 
    // e.g., show a `UIActivityIndicatorView`

    [self doSomethingToGetKeyWithCompletionHandler:^(NSString *key){

        // remove that `UIActivityIndicatorView`, if you showed one

        [self performSegueWithIdentifier:@"Details" sender:self];
    }];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"Details"]) {
        [segue.destinationViewController setKey:_key]
    }
}

Alternatively, you can remove all of this, just perform the segue, but then have the destination view controller present itself, show the activity indicator view, perform the api calls, and remove the activity indicator view. This is a more dramatic refactoring of the code, but is probably a better UX.

But, bottom line, from within prepareForSegue you cannot do something asynchronous, and expect the destination view controller to wait for it, even if you employ the completion block pattern. You'd have to use the IBAction approach if you want to do all of this before performing the segue. Or, like I suggested here, just do all of this asynchronous request stuff in the destination view controller. For some reason, transitioning immediately to the destination view controller (and showing the UIActivityIndicatorView there) is more satisfying UX than delaying the presentation of the destination view controller (even though they're functionally very similar).

Upvotes: 3

bartl
bartl

Reputation: 392

You need to change your code to something like that:

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{

   [self doSomethingToGetKey:account completion:^{
      [segue.destinationViewController setKey:_key];
   }];

}

-(void)doSomethingToGetKey:(ACAccount *)account completion:(void (^)(void)completion{

[_apiObject postReverseOAuthTokenRequest:^(NSString *authenticationHeader) {

    APIObject *api = [APIObject APIOSWithAccount:account];
    [api verifyCredentialsWithSuccessBlock:^(NSString *username) {


        [api postReverseAuthAccessTokenWithAuthenticationHeader:authenticationHeader successBlock:^(NSString *oAuthToken, NSString *oAuthTokenSecret, NSString *userID, NSString *screenName) {

            _key = oAuthToken;
            _secret = oAuthTokenSecret;
            _userId = userID;

            if (completion)
               completion();

        } errorBlock:^(NSError *error) {

            NSLog(@"ERROR 1, %@", [error localizedDescription]);
            exit(1);

        }];

    } errorBlock:^(NSError *error) {

        NSLog(@"ERROR 2: %@",error.description);
        exit(1);

    }];

} errorBlock:^(NSError *error) {

    NSLog(@"ERROR 3: %@",error.description);
    exit(1);

}];

};

Upvotes: 2

Related Questions