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

2

9

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?

Scrimp answered 5/3, 2013 at 19:19 Comment(2)
I haven't run into this situation, but it seems like an NSFileCoordinator might do the job?Brandi
@MikeM You may be right in that it would prevent the crash, but I'm worried that it has the potential to really slow things down. Often updates in the app are small & frequent, and up-to-date content is required for the app to stay responsive. I'll have to investigate this approach further and see if it's viable. However the question still remains - is the sanctioned approach to UIDocument usage not thread safe?Scrimp
R
8

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.

Rehm answered 12/3, 2013 at 3:24 Comment(7)
Thanks for your reply. I already understand most of what you've mentioned (but it's nice to have it stated succinctly here), e.g. I'm overriding writeContents:... and calling its super implementation in order to implement preview saving, and I'm using updateChangeCount: to flag required saves. I'm also aware that UIDocument handles the file coordination, however doesn't coordinated writing on the root file wrapper imply coordination on the subfiles? From the File System Programming Guide: "Note: When an NSFileWrapper instance is specified as the item for coordination, all the files...Scrimp
...within the file wrapper are automatically part of that file coordination.". I'm currently using a separate data object to store the data (in Page instances owned by my UIDocument subclass), but as soon as a change is made I'm putting a new file wrapper under the root file wrapper as is done in Apple's CloudNotes sample app, rather than waiting and adding it in contentsForType:. As you point out, this is surely the problem. I'll defer updates to the root file wrapper until UIDocument asks for a snapshot, and see if all works well. Thanks again.Scrimp
The documentation is confusing and I had problems as you did. So I figured I'd cover as much as I could. You probably want to do preview saving somewhere else. CloudNotes does some wacky stuff - it saves a second UIDocument for a preview. You really only need to do that if you aren't always keeping a local copy of each document. Yes, if you coordinate the root document it can apply to subfiles, but, as far as I know, thats assuming the other app/object coordinates the root document (iCloud & UIDocument does this).Rehm
If you need clarification on anything UIDocument related let me know. I spent a ton of time figuring this stuff out.Rehm
Thanks very much. I can certainly see why it would have taken you a ton of time!Scrimp
I need my preview to be available for documents that haven't yet been downloaded from iCloud, so I just save out a little file package with a plist and a preview image to the data directory. I don't appear to have any problems relating to that code. I'll update the question/answer when I have a moment to try the new page updating mechanism mentioned.Scrimp
I've open sourced by file wrapper here: github.com/lukescott/LSFileWrapperRehm
S
0

I know this is an ancient thread, but I ran into this problem recently, and to help future travelers: If you have subdirectories in your main file wrapper, you need to copy those NSFileWrappers as well (in addition to copying the root NSFileWrapper as above).

Otherwise, a crash can occur when the UIDocument background thread enumerates over them while saving and while simultaneous modifications occur on the main thread. It's not clear, but this might be the problem the OP ran into.

Another tip is you need to also copy over the subdirectory's NSFileWrapper's filename, fileAttributes (and possibly preferredFilename) so that incremental saving works.

HTH.

Speculative answered 16/10, 2019 at 3:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.