NSFileWrapper, lazy loading and saving
Asked Answered
A

3

13

I have an NSDocument based application that uses filewrappers to save and load its data. The document can have all kinds of resources, so I don't want to load everything into memory. I might be doing something fundamentally wrong, but as soon as I change one (inner) file and then save, I can't read any file that hasn't been loaded into memory.

I have separated the relevant code into a separate project to reproduce this behaviour, and I get the same results. The basic flow is this:

  • I load an existing document from disk. The main fileWrapper is a directory filewrapper (I'll call that main) containing two other filewrappers (sub1 and sub2). The two inner filewrappers are not loaded at this point.
  • When the user wants to edit sub1, it is loaded from disk.
  • The user saves the document
  • If the user wants to edit the other file (sub2), it cannot load. The error that appears:

    -[NSFileWrapper regularFileContents] tried to read the file wrapper's contents lazily but an error occurred: The file couldn’t be opened because it doesn’t exist.
    

Here is the relevant code in my project:

This code might be easier to read in this gist: https://gist.github.com/bob-codingdutchmen/6869871

#define FileName01 @"testfile1.txt"
#define FileName02 @"testfile2.txt"

/**
 *  Only called when initializing a NEW document
 */
-(id)initWithType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
    self = [self init];
    if (self) {
        self.myWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];

        NSLog(@"Initializing new document...");

        NSString *testString1 = @"Lorem ipsum first sub file";
        NSString *testString2 = @"This is the second sub file with completely unrelated contents";

        NSFileWrapper *w1 = [[NSFileWrapper alloc] initRegularFileWithContents:[testString1 dataUsingEncoding:NSUTF8StringEncoding]];
        NSFileWrapper *w2 = [[NSFileWrapper alloc] initRegularFileWithContents:[testString2 dataUsingEncoding:NSUTF8StringEncoding]];

        w1.preferredFilename = FileName01;
        w2.preferredFilename = FileName02;

        [self.myWrapper addFileWrapper:w1];
        [self.myWrapper addFileWrapper:w2];

    }
    return self;
}

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

    // This obviously wouldn't happen here normally, but it illustrates
    // how the contents of the first file would be replaced
    NSFileWrapper *w1 = [self.myWrapper.fileWrappers objectForKey:FileName01];
    [self.myWrapper removeFileWrapper:w1];

    NSFileWrapper *new1 = [[NSFileWrapper alloc] initRegularFileWithContents:[@"New file contents" dataUsingEncoding:NSUTF8StringEncoding]];
    new1.preferredFilename = FileName01;

    [self.myWrapper addFileWrapper:new1];

    return self.myWrapper;
}

-(BOOL)readFromFileWrapper:(NSFileWrapper *)fileWrapper ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError {
    self.myWrapper = fileWrapper;
    return YES;
}

- (IBAction)button1Pressed:(id)sender {
    // Read from file1 and show result in field1
    NSFileWrapper *w1 = [[self.myWrapper fileWrappers] objectForKey:FileName01];
    NSString *string1 = [[NSString alloc] initWithData:w1.regularFileContents encoding:NSUTF8StringEncoding];
    [self.field1 setStringValue:string1];
}

- (IBAction)button2Pressed:(id)sender {
    // Read from file2 and show result in field2
    NSFileWrapper *w2 = [[self.myWrapper fileWrappers] objectForKey:FileName02];
    NSString *string2 = [[NSString alloc] initWithData:w2.regularFileContents encoding:NSUTF8StringEncoding];
    [self.field2 setStringValue:string2];
}

The bottom two methods are only for updating the UI so I can see what happens.

  • To change the contents of a file, I remove the existing fileWrapper and add a new one. This is the only way I've found to change the contents of a file, and the way I've seen it done in other SO answers.

  • When a document is loaded from disk, I keep the fileWrapper around so I can use it (called myWrapper in the code above)

The Apple docs say that NSFileWrapper supports lazy loading and incremental saving, so I'm assuming that my code has some fundamental flaw that I can't see.

Afterworld answered 7/10, 2013 at 15:36 Comment(8)
Does your document support versioning?Abyss
I'm not sure, so I think not :) I haven't done anything to support it. Would it change anything? I'll look into that laterAfterworld
As I'm using an NSDocument setup, I was supporting versioning. I've tried turning it off, but the result is the sameAfterworld
Have you tried going on the official Apple developer forums? You'll get experienced people there IMAO.Housekeeper
@Housekeeper What does IMAO stand for?Flavia
I'm running into the exact same problem -- curious to know if you ever figured out how to fix this.Overtask
Did you ever figure out a solution? Starting with macOS High Sierra, my app now shows the same error. It worked fine before...Cedell
I have since moved on from Mac app development, never found a solution for this unfortunately...Afterworld
A
1

An NSFileWrapper is essentially a wrapper around a unix file node. If the file is moved the wrapper stays valid.

The problem yo seem to have is that creating a new file wrapper during saving is a new folder. And the system deletes your previous wrapper including sub2.

To achieve what you want you need to change to incremental saving, i.e. Only saving changed parts in place. See "save in place" in NSDocument.

Altruism answered 11/11, 2013 at 6:39 Comment(0)
W
1

In your -fileWrapperOfType:error: method, try building a new file wrapper that has new contents for the changed members and references the old file wrappers for the unchanged members.

Walton answered 14/1, 2014 at 0:45 Comment(0)
W
0

Following the documentation to addFileWrapper: you add a child (subdirectory) to it, means

directory/

  1. addfileWrapper:fileName1 directory/fileName1/

  2. addfileWrapper:fileName2 directory/fileName1/fileName2. That file doesn't exist.

You have to use addRegularFileWithContents:preferredFilename: instead.

Womble answered 26/3, 2014 at 16:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.