Wait for async task to finish completion block before returning in app delegate
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.
Related videos on Youtube
MLQ
Updated on July 09, 2022Comments
-
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 methodprepareWithCompletionHandler:
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 eitherYES
orNO
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 returningsuccessful
? I'm really confused.-
Cy-4AH about 10 yearsAlso, 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 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 about 10 yearsI'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 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 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 about 10 yearsIf it cause deadlock, then you don't need semaphore, yours method prepareWithCompletionHandler is sinchronous.
-
MLQ about 10 years@Cy-4AH The problem though is that the app delegate doesn't wait for the
prepare
's completion block before returningdidFinishLaunchingWithOptions:
. -
Cy-4AH about 10 years@MattQuiros, could you add
openWithCompletionHandler
andsaveToURL
code to make sure, thatprepareWithCompletionHandler
work asynchronously? -
MLQ about 10 yearsAlso causes a deadlock. :(
-
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 about 10 years
open...
andsaveToURL:...
are inherited methods fromUIDocument
whichUIManagedDocument
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/reference/… -
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 about 10 years@MattQuiros, I have edited post, it should solve deadlock problem.
-
MLQ about 10 yearsThanks, but I'm afraid the deadlock still occurs whenever
openWithCompletionHandler:
orsaveToURL:
calls their callback methods inprepareWithCompletionHandler:
. 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 about 10 yearsWait, 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 about 10 yearsYou 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 about 10 years@MattQuiros, there was deadlock because
open
andsave
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 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.