38

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.

Matthew Quiros
  • 13,385
  • 12
  • 87
  • 132
  • 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. – Cy-4AH Apr 07 '14 at 15:19
  • `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. – David Berry Apr 07 '14 at 15:23

3 Answers3

46

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • 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? – Matthew Quiros Apr 07 '14 at 14:23
  • 1
    @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. – Rob Apr 07 '14 at 14:27
  • ^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. – Matthew Quiros Apr 07 '14 at 14:29
  • 1
    If it cause deadlock, then you don't need semaphore, yours method prepareWithCompletionHandler is sinchronous. – Cy-4AH Apr 07 '14 at 14:31
  • @Cy-4AH The problem though is that the app delegate doesn't wait for the `prepare`'s completion block before returning `didFinishLaunchingWithOptions:`. – Matthew Quiros Apr 07 '14 at 14:33
  • @MattQuiros, could you add `openWithCompletionHandler` and `saveToURL` code to make sure, that `prepareWithCompletionHandler` work asynchronously? – Cy-4AH Apr 07 '14 at 14:37
  • @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). – Rob Apr 07 '14 at 14:39
  • `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. https://developer.apple.com/library/ios/documentation/uikit/reference/UIDocument_Class/UIDocument/UIDocument.html – Matthew Quiros Apr 07 '14 at 14:42
  • 1
    @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! – Rob Apr 07 '14 at 14:45
  • 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. – gnasher729 Apr 07 '14 at 15:07
  • @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. – Rob Apr 07 '14 at 15:29
5

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;
}
Forge
  • 6,538
  • 6
  • 44
  • 64
Cy-4AH
  • 4,370
  • 2
  • 15
  • 22
  • Also causes a deadlock. :( – Matthew Quiros Apr 07 '14 at 14:37
  • @MattQuiros, I have edited post, it should solve deadlock problem. – Cy-4AH Apr 07 '14 at 14:49
  • 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. – Matthew Quiros Apr 07 '14 at 14:55
  • 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... – Matthew Quiros Apr 07 '14 at 14:57
  • @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. – Cy-4AH Apr 07 '14 at 15:12
5

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.

David Berry
  • 40,941
  • 12
  • 84
  • 95