Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIManagedDocument Singleton Code openWithCompletionHandler called twice and crashes

I am using Justin Driscoll's implementaion on Core Data with a Single Shared UIManagedDocument. Everything was fine in my iphone app until I moved it to a iPad storyboard and a splitview controller for the ipad app. The problem is openwithCompletionHandler is being called twice, once from my master view in viewDidLoad and again in my detail view viewWillLoad. The calls are in quick succession and since the document is still in UIDocumentStateClosed when the second call is made to my performWithDocument method (below) of the singleton the app crashes. I looked at e_x_p ' s answer for post iOS5.1: synchronising tasks (wait for a completion) but @sychronized will not work in this case since performWithDocument below is called on the same thread. How would I protect against multiple calls to openwithCompletionHandler? The only way I can think to protect against this is to pause execution of one of the calls above until i am sure UIDocumentStateNormal is true and then release. That though would freeze the main UI thread which is not good. What though would be the best way todo this without freezing up the UI?

From the UIManagedDocumentSingleton code:

- (void)performWithDocument:(OnDocumentReady)onDocumentReady
{
    void (^OnDocumentDidLoad)(BOOL) = ^(BOOL success)
    {
        onDocumentReady(self.document);
    };

    if (![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]])
    {
        //This should never happen*******************
        [self.document saveToURL:self.document.fileURL
                forSaveOperation:UIDocumentSaveForCreating
               completionHandler:OnDocumentDidLoad];

    } else if (self.document.documentState == UIDocumentStateClosed) {
        [self.document openWithCompletionHandler:OnDocumentDidLoad];
    } else if (self.document.documentState == UIDocumentStateNormal) {
        OnDocumentDidLoad(YES);
    }
}
like image 219
William Bagdan Avatar asked Nov 20 '12 19:11

William Bagdan


2 Answers

I did it as Justin suggested above below. Works fine in one of my apps for two years with ~20k users.

@interface SharedUIManagedDocument ()  
@property (nonatomic)BOOL preparingDocument; 
@end

- (void)performWithDocument:(OnDocumentReady)onDocumentReady
{
    void (^OnDocumentDidLoad)(BOOL) = ^(BOOL success) {
        onDocumentReady(self.document);
        self.preparingDocument = NO; // release in completion handler
    };

    if(!self.preparingDocument) {
        self.preparingDocument = YES; // "lock", so no one else enter here
        if(![[NSFileManager defaultManager] fileExistsAtPath:[self.document.fileURL path]]) {
            [self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:OnDocumentDidLoad];
        } else if (self.document.documentState == UIDocumentStateClosed) {
            [self.document openWithCompletionHandler:OnDocumentDidLoad];
        } else if (self.document.documentState == UIDocumentStateNormal) {
            OnDocumentDidLoad(YES);
        }
    } else {
        // try until document is ready (opened or created by some other call)
        [self performSelector:@selector(performWithDocument:) withObject:onDocumentReady afterDelay:0.5];
    }
}

Swift (not much tested)

typealias OnDocumentReady = (UIManagedDocument) ->()

class SharedManagedDocument {

private let document: UIManagedDocument
private var preparingDocument: Bool

static let sharedDocument = SharedManagedDocument()

init() {
    let fileManager = NSFileManager.defaultManager()
    let urls = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)
    let documentsDirectory: NSURL = urls.first as! NSURL
    let databaseURL = documentsDirectory.URLByAppendingPathComponent(".database")
    document = UIManagedDocument(fileURL: databaseURL)
    let options = [NSMigratePersistentStoresAutomaticallyOption : true, NSInferMappingModelAutomaticallyOption : true]
    document.persistentStoreOptions = options
    preparingDocument = false
}

func performWithDocument(onDocumentReady: OnDocumentReady) {

    let onDocumentDidLoad:(Bool) ->() = {
        success in
        onDocumentReady(self.document)
        self.preparingDocument = false
    }
    if !preparingDocument {
        preparingDocument = true
        if !NSFileManager.defaultManager().fileExistsAtPath(document.fileURL.path!) {
            println("Saving document for first time")
            document.saveToURL(document.fileURL, forSaveOperation: .ForCreating, completionHandler: onDocumentDidLoad)
        } else if document.documentState == .Closed {
            println("Document closed, opening...")
            document.openWithCompletionHandler(onDocumentDidLoad)
        } else if document.documentState == .Normal {
            println("Opening document...")
            onDocumentDidLoad(true)
        } else if document.documentState == .SavingError {
            println("Document saving error")
        } else if document.documentState == .EditingDisabled {
            println("Document editing disabled")
        }
    } else {
        // wait until document is ready (opened or created by some other call)
        println("Delaying...")
        delay(0.5, closure: {
            self.performWithDocument(onDocumentReady)
        })
    }
}

private func delay(delay:Double, closure:()->()) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), closure)
}
}
like image 185
Vladimir Shutyuk Avatar answered Nov 10 '22 10:11

Vladimir Shutyuk


That's interesting and definitely a flaw in my code (sorry!). My first thought would be to add a serial queue as a property to your document handler class and perform the check on that.

self.queue = dispatch_queue_create("com.myapp.DocumentQueue", NULL);

and then in performWithDocument:

dispatch_async(self.queue, ^{
    if (![[NSFileManager defaultManager] fileExistsAtPath... // and so on
});

But that wouldn't work either...

You could set a BOOL flag when you call saveToURL and clear it in the callback. Then you can check for that flag and use performSelectorAfterDelay to call performWithDocument again a little later if the file is being created.

like image 1
Justin Driscoll Avatar answered Nov 10 '22 08:11

Justin Driscoll