I have a UIDocument
based app that uses NSFileWrapper
s 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:
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?
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.
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