NSDocument-based Mac application with bundles for document, super slow to save
Asked Answered
E

2

6

So after googling a bit it's my understanding that when I use a bundle for my document in a NSDocument-based app I have to write the entire bundle each time I want to save? This is starting to get problematic here as I'm adding large video files to the document.

Is there any way around this without throwing out NSDocument?

Ecclesiasticism answered 27/5, 2013 at 12:34 Comment(0)
A
11

One of the advantages of using document packages is that you do not need to write everything each time you save. You should add a documentFileWrapper property to your NSDocument subclass and, when the document architecture reads in a document package, you should store an NSFileWrapper reference in that property, and you should also keep track of what has been changed. For example,aAssuming you’re keeping a text file and a video file in your document package:

@interface MyDocument ()
    @property (nonatomic, copy) NSString *text;
    @property (nonatomic, assign) bool textHasChanged;

    @property (nonatomic, copy) NSData *videoData;
    @property (nonatomic, assign) bool videoHasChanged;

    @property (nonatomic, strong) NSFileWrapper *documentFileWrapper;
@end

and

- (BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper
                     ofType:(NSString *)typeName
                      error:(NSError **)outError {


    // Read the contents of file wrapper
    // …

    [self setDocumentFileWrapper:fileWrapper];

    return YES;
}

If a document package was read from disk, documentFileWrapper keeps a reference to that package. If it’s a new document, documentFileWrapper is nil.

When saving the document, you reuse documentFileWrapper and write only the files that need saving. In case the document hasn’t been saved yet (it’s a new document), documentFileWrapper is nil, so you need to create one:

- (NSFileWrapper *)fileWrapperOfType:(NSString *)typeName
                               error:(NSError **)outError
{

    if ([self documentFileWrapper] == nil) {
        NSFileWrapper *documentFileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:@{}];
        [self setDocumentFileWrapper:documentFileWrapper];
    }

    NSDictionary *fileWrappers = [[self documentFileWrapper] fileWrappers];
    NSFileWrapper *textFileWrapper = [fileWrappers objectForKey:@"text.txt"];
    NSFileWrapper *videoFileWrapper = [fileWrappers objectForKey:@"video.mp4"];

    if ((textFileWrapper == nil && [self text] != nil) || [self textHasChanged]) {
        if (textFileWrapper != nil)
            [documentFileWrapper removeFileWrapper:textFileWrapper];

        if ([self text] != nil) {
            NSData *textData = [[self text] dataUsingEncoding:NSUTF8StringEncoding];
            [documentFileWrapper addRegularFileWithContents:textData preferredFileName:@"text.txt"];
        }
    }

    if ((videoFileWrapper == nil && [self videoData] != nil) || [self videoHasChanged]) {
        if (videoFileWrapper != nil)
            [documentFileWrapper removeFileWrapper:videoFileWrapper];

        if ([self videoData] != nil)
            [documentFileWrapper addRegularFileWithContents:[self videoData] preferredFileName:@"video.mp4"];
    }

    return documentFileWrapper;
}

In the code above, the document package (file wrapper) is created only if it doesn’t already exist. In other words, it is created only if the document is new and hasn’t been saved yet. After the package is created, its reference is stored in documentFileWrapper.

Next, we check whether we need to write the text file. It is written if: * either there is no text file yet (probably because it’s a new package) and we have actual text to write; * or if the text has been changed.

We do the same for the video file.

Note that if the document has been loaded from disk or saved at least once, the video file is added to the package (and the previous video file, if any, is removed from the package) only if the video data has been changed.

NB: this answer assumes ARC and automatic property synthesis (ivar names prefixed with an underscore).

Adigranth answered 5/6, 2013 at 7:27 Comment(5)
That's a great answer! And seem to do exactly what I was planning to do. Before I kept returning a new filewrapper from fileWrapperOfType:error:. However, every now and then (randomly it seems), the save operation takes much longer than it should though, disregarding if I've added or changed anything. I've checked the datestamps on the files in the bundle, and it's not overwriting anything.Rustproof
So here's the really weird part. If I make a change that doesn't trigger the "Edited" status of the document (don't do updateChangeCount:), it saves really quickly. And I can close it and open it again and the change is there. Now, the first time I save, and when I save after the document has changed status to edited it takes a long time again. But from what I can tell after looking inside the bundle, and after examining the files, there is no difference in these two saves. Just that one takes much longer than the other. Any idea what might be going on here?Rustproof
So I figured it out here. Versions was causing the slow saving. Disabled that with preservesVersions returning NO, and now it works perfectly.Rustproof
@ChristianA.Strømmen Interesting. I’m not sure what’s the best strategy for versions and large files. This thread shows a similar issue, but unfortunately no solution was posted.Adigranth
@Bavarious how would I do this if videoData is an NSURL to a temporary file that is copied to the correct location on save? I wouldn't actually want to store video data in memory.Ibnsina
L
4

I know this has been answered, but as reference, if you take a look at Apple's Packaged Document sample code, you'll see a variant of this that works without having to keep a changed flag for every instance variable.

The basic idea is:

  • If a model variable changes, remove it's corresponding file wrapper from the main file wrapper
  • When building the file wrapper in fileWrapperOfType:… only add those that are missing (i.e. were updated)
Ladle answered 8/1, 2014 at 16:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.