Wait for async task to finish completion block before returning in app delegate

41,903

Solution 1

I'm unsure why the didFinishLaunching return status is dependent upon the success of your completion handler as you're not apparently even considering launchOptions. I'd hate to see you put an synchronous call (or more accurately, use a semaphore to convert an asynchronous method into a synchronous one) here, as it will slow down the app and, if its slow enough, you risk being killed by the watch dog process.

Semaphores are one common technique for making an asynchronous process synchronous:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    __block BOOL successful;
    SPRManagedDocument *document = [SPRManagedDocument sharedDocument];

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    [document prepareWithCompletionHandler:^(BOOL success) {
        successful = success;
        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return successful;
}

But, upon further review of what prepareWithCompletionHandler is doing, it's apparently calling methods that dispatch their own completion blocks to the main queue, so any attempts to make this synchronous will deadlock.

So, use asynchronous patterns. If you want to initiate this in the didFinishLaunchingWithOptions, you can have it post a notification:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    __block BOOL successful;
    SPRManagedDocument *document = [SPRManagedDocument sharedDocument];

    [document prepareWithCompletionHandler:^(BOOL success) {
        successful = success;
        [[NSNotificationCenter defaultCenter] postNotificationName:kDocumentPrepared object:nil];
    }];

    return successful;
}

And you can then have your view controller addObserverForName to observe this notification.

Alternatively, you can move this code out of the app delegate and into that view controller, eliminating the need for the notification.

Solution 2

For your case using dispatch group will be slightly different:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    __block BOOL successful;
    SPRManagedDocument *document = [SPRManagedDocument sharedDocument];

    dispatch_group_t group = dispatch_group_create();
    dispatch_group_enter(group);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [document prepareWithCompletionHandler:^(BOOL success) {
            successful = success;
            dispatch_group_leave(group);
        }];
    }];

    dispatch_group_wait(group,  DISPATCH_TIME_FOREVER);
    return successful;
}

Solution 3

A lot of proposed solutions here using either dispatch_group_wait or semaphores, but the real solution is to rethink why you want to block from returning didFinishLaunching until after a possibly lengthy asynchronous request completes. If you really can't usefully do anything else until the operation completes, my recommendation would be to display some sort of a loading please wait screen while the initialization happens and then immediately return from didFinishLaunching.

Share:
41,903

Related videos on Youtube

MLQ
Author by

MLQ

Updated on July 09, 2022

Comments

  • MLQ
    MLQ almost 2 years

    I'm using a subclass of UIManagedDocument to use Core Data in my project. The point is for the subclass to return a singleton instance so that my screens can simply call it and the managed object context remains the same for all of them.

    Before using the UIManagedDocument, I need to prepare it by opening it if its file path already exists, or creating it if it doesn't yet. I created a convenience method prepareWithCompletionHandler: in the subclass to facilitate both scenarios.

    @implementation SPRManagedDocument
    
    // Singleton class method here. Then...
    
    - (void)prepareWithCompletionHandler:(void (^)(BOOL))completionHandler
    {
        __block BOOL successful;
    
        // _exists simply checks if the document exists at the given file path.
        if (self.exists) {
            [self openWithCompletionHandler:^(BOOL success) {
                successful = success;
    
                if (success) {
                    if (self.documentState != UIDocumentStateNormal) {
                        successful = NO;
                    }
                }
                completionHandler(successful);
            }];
        } else {
            [self saveToURL:self.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
                successful = success;
    
                if (success) {
                    if (self.documentState != UIDocumentStateNormal) {
                        successful = NO;
                    }
                }
                completionHandler(successful);
            }];
        }
    }
    
    @end
    

    What I'm trying to do is call this preparation method in my app delegate's didFinishLaunchingWithOptions and wait for the completion block to be executed BEFORE returning either YES or NO at the end. My current approach doesn't work.

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    {
        __block BOOL successful;
        SPRManagedDocument *document = [SPRManagedDocument sharedDocument];
    
        dispatch_group_t group = dispatch_group_create();
    
        dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [document prepareWithCompletionHandler:^(BOOL success) {
                successful = success;
            }];
        });
    
        dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        });
    
        return successful;
    }
    

    How can I wait until the completion handler in prepareWithCompletionHandler is called before returning successful? I'm really confused.

    • Cy-4AH
      Cy-4AH about 10 years
      Also, why you wish wait prepare document completion? You doesn't use launchOptions and don't need report to IOS handled you openURL request or not. Use - (void)applicationDidFinishLaunching:(UIApplication *)application and don't wait for completion.
    • David Berry
      David Berry about 10 years
      dispatch_group_notify doesn't work because it just schedules a block to be executed when all the prior operations on the group are completed and then immediately returns. dispatch_group_wait will do what you expect.
  • MLQ
    MLQ about 10 years
    I'm not sure either--I should probably put that back to YES. But the point is for the document to be ready even before showing the app's UI. Or should I do that in the first view controller?
  • Rob
    Rob about 10 years
    @MattQuiros I'm OK with starting the asynchronous "prepare" process here, but I wouldn't wait for it, but rather I might have the completion block post a local notification, and have the first view controller observe for that notification, not initiating the steps contingent on that process until the notification comes in.
  • MLQ
    MLQ about 10 years
    ^Great tip. I just tried your solution by the way, and it's causing a deadlock. :( I've tried semaphores even before dispatch groups, actually.
  • Cy-4AH
    Cy-4AH about 10 years
    If it cause deadlock, then you don't need semaphore, yours method prepareWithCompletionHandler is sinchronous.
  • MLQ
    MLQ about 10 years
    @Cy-4AH The problem though is that the app delegate doesn't wait for the prepare's completion block before returning didFinishLaunchingWithOptions:.
  • Cy-4AH
    Cy-4AH about 10 years
    @MattQuiros, could you add openWithCompletionHandler and saveToURL code to make sure, that prepareWithCompletionHandler work asynchronously?
  • MLQ
    MLQ about 10 years
    Also causes a deadlock. :(
  • Rob
    Rob about 10 years
    @MattQuiros Deadlocks will happen if the process is synchronous (and to Cy-4AH's point, just because there's a completion block doesn't mean it's necessarily asynchronous) or if you're it's otherwise dependent on the main queue (e.g. you've got some dispatch_sync to the main queue buried in there somewhere in your asynchronous process). You should confirm it's asynchronous, and if so, confirm there are no synchronous dispatches back to the main queue (either explicit or implicit).
  • MLQ
    MLQ about 10 years
    open... and saveToURL:... are inherited methods from UIDocument which UIManagedDocument inherits from. I can only trust the docs that they are asynchronous. However, there probably is a dispatch back to the main queue, also according to the docs. developer.apple.com/library/ios/documentation/uikit/referenc‌​e/…
  • Rob
    Rob about 10 years
    @MattQuiros Yep, then that means that you cannot run this synchronously (you can't block the main queue, waiting for something that, itself, will be run on the main queue). To our earlier discussion, you probably don't want to do that, anyway. Go async!
  • Cy-4AH
    Cy-4AH about 10 years
    @MattQuiros, I have edited post, it should solve deadlock problem.
  • MLQ
    MLQ about 10 years
    Thanks, but I'm afraid the deadlock still occurs whenever openWithCompletionHandler: or saveToURL: calls their callback methods in prepareWithCompletionHandler:. As @Rob said, I can't block the main queue, and I think the open and create methods, being async, are dispatching back to the main queue internally to execute their completion blocks. I should probably find another way around this.
  • MLQ
    MLQ about 10 years
    Wait, no, they should be dispatched back to the thread that called prepareWithCompletionHandler, right? This is getting even more confusing. Unless perhaps I create my own queue...
  • gnasher729
    gnasher729 about 10 years
    You are not supposed to wait for an async task to finish. If you wait for it, why not make it synchronous? It's the whole point of an async task to do its thing in the background, and then cause some action when it is finished.
  • Cy-4AH
    Cy-4AH about 10 years
    @MattQuiros, there was deadlock because open and save was performed in serial queue and completion was waited in same serial queue. If you call them in other concurent queue, then all will be fine.
  • Rob
    Rob about 10 years
    @gnasher729 I agree. There are some isolated cases where you might need to make an asynchronous process synchronous, but this isn't one, IMHO. In his original code sample, he implied that the return code had to be contingent upon the success of the asynchronous process initiated by prepareWithCompletionHandler. In a later comment, though, he clarified that the return code really wasn't dependent upon this method, in which case he should't be using semaphores or anything like that and he should embrace the asynchronous nature of this call.