Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIDocument & NSFileWrapper - NSFastEnumerationMutationHandler while changing file wrapper during a save

I have a UIDocument based app that uses NSFileWrappers to store data. The 'master' file wrapper contains many additional directory file wrappers, each of which represents a different page of the document.

Whenever I make a change to the document while the UIDocument is saving (in writeContents:andAttributes:safelyToURL:forSaveOperation:error:), the app crashes. Here is the stack trace:

UIDocument crash stack trace

It seems clear that I am modifying the same instance of file wrapper that the UIDocument is enumerating over in the background. Indeed, I checked that when returning a snapshot of the data model in contentsForType:error:, the returned sub file wrappers point to the same objects as the ones currently residing (and being edited) in the data model, and not copies.

- (id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
    if (!_fileWrapper) {
        [self setupEmptyDocument];
    }
    return [[NSFileWrapper alloc] initDirectoryWithFileWrappers:[_fileWrapper fileWrappers]];
}

This is the sanctioned approach to implementing this method (according to WWDC 2012 Session 218 - Using iCloud with UIDocument).

So I suppose the question is: How can this approach be thread safe?

Is the situation somehow different when the master file wrapper's fileWrappers are themselves directory file wrappers? If the sanctioned approach is wrong, how should it be done?

like image 797
Stuart Avatar asked Mar 05 '13 19:03

Stuart


1 Answers

If you are calling any of the writeContents:... methods, you shouldn't be. You should be calling saveToURL:forSaveOperation:completionHandler: instead. The writeContents:... methods are meant for advanced subclassing.

UIDocument uses two threads - the main thread and the "UIDocument File Access" thread (which , if you subclass more of UIDocument, you can do things in via performAsynchronousFileAccessUsingBlock:).

Thread safety with UIDocument is like anything in Objective C - only let the thread owning an object modify it. If the object you want to change is being read, queue it to be changed after the write is complete. Perhaps change a different object owned by your UIDocument subclass and pull them into a new NSFileWrapper in contentsForType:error:. Pass a copy of the fileWrappers NSDictionary.

NSFileWrapper actually loads the entire document into memory. The NSFileWrapper is actually created in the "UIDocument File Access" thread in the readFromURL:error: method, which is then passed to the loadFromContents:ofType:error: method. If you have a large document this can take a while.

When saving you typically want to let UIDocument decide when to do this, and let it know something has changed via the updateChangeCount: method (param is UIDocumentChangeDone). When you want to save something right now you want to use the saveToURL:forSaveOperation:completionHandler: method.

One other thing to note is UIDocument implements the NSFilePresenter protocol, which defines methods for NSFileCoordinator to use. The UIDocument only coordinates writing on the root document, not the subfiles. You might think that coordinating subfiles inside the document might help, but the crash you're getting is related to mutating a dictionary while it's being iterated, so that wont help. You only need to worry about writing your own NSFilePresenter if you (1) wanted to get notifications of file changes, or (2) another object or app was reading/writing to the same file. What UIDocument already does will work fine. You do want to, however, use NSFileCoordinator when moving/deleting whole documents.

like image 58
Luke Avatar answered Oct 31 '22 08:10

Luke