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);
}
}
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)
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With