UIManagedDocument Singleton Code openWithCompletionHandler is called twice and crashes

I use the implementation of Justin Driscoll on Master Data with a single shared UIManagedDocument . Everything was in my iphone application until I moved it to the iPad storyboard and the splitview controller for the ipad application. The openwithCompletionHandler problem is called twice, once from my main view in viewDidLoad and again in my detailed view viewWillLoad. Calls are made quickly, and since the document is still in UIDocumentStateClosed, when the second call is made in my executeWithDocument method (below) of the single, the application shuts down. I looked at the e_x_p answer on the iOS5.1 post : task synchronization (wait for completion) , but @psychronized will not work in this case, since the executeWithDocument function is called below the same thread. How to protect multiple calls from openwithCompletionHandler? The only way I can protect against this is to pause one of the calls above until I am sure that the UIDocumentStateNormal is true and then freed. This, at least freeze the main thread of the user interface, which is not very good. What would be the best way to do this without freezing the user interface?

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); } } 
+6
source share
3 answers

I did this, as Justin suggested above below. Works fine in one of my applications for two years with ~ 20 thousand 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 (little 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) } } 
+2
source

This is an interesting and definitely flaw in my code (sorry!). My first thought was to add a serial queue as a property to the document handler class and check this out.

 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 won't work either ...

You can set the BOOL flag when calling saveToURL and clear it in the callback. You can then check this flag and use the performSelectorAfterDelay function to call the executeWithDocument function again a little later if the file is created.

+1
source

A block of code that is divided between numberOfRowsInSection: and cellForRowAtIndexPath: should only be called once. numberOfRowsInSection always called before the tableView tries to map the cells, so you must create an NSArray object into which you can save the results of the select query, and then use this array when rendering your cells:

 @implementation FooTableViewController { NSArray *_privateArray; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { [[UIManagedDocumentSingletonHandler sharedDocumentHandler] performWithDocument:^(FCUIManagedDocumentObject *document) { NSManagedObjectContext * context = document.managedObjectContext; NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"FCObject"]; NSPredicate * searchStringPredicate = nil; if (searchFilterString) { searchStringPredicate = [NSPredicate predicateWithFormat:@"word BEGINSWITH[c] %@",searchFilterString]; } request.predicate = searchStringPredicate; request.shouldRefreshRefetchedObjects = YES; NSError * error; _privateArray = [context executeFetchRequest:request error:&error]; }]; return _privateArray.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"FCCell"; FCardCell *cell = (FCCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; // Configure the cell... FCManagedObject * fcc = [_privateArray objectAtIndex:indexPath.row]; cell.isWordVisible.on = fcc.isUsed; cell.fWord.text = fcc.word; return cell; } 

I'm not sure about the top of my head if you need to do something special with NSArray to set it inside a block (a la __block ).

The main reason for this is that you need to make sure that in 100% of cases when the dataset used to determine the number of rows is the same size as when you created your cells. If they do not match, you will crash. In addition, since you do not have a block, you do not need to send UITableViewCell updates.

Finally, if the UIDocumentStateClosed is causing problems, you must either filter them out of your NSFetch results (an additional predicate, if required) or have code to manage them more efficiently in cellForRowAtIndexPath:

0
source

All Articles