Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSFileWrapper, lazy loading and saving

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.

like image 784
Bob Vork Avatar asked Oct 07 '13 15:10

Bob Vork


2 Answers

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.

like image 137
Cocoanetics Avatar answered Oct 24 '22 04:10

Cocoanetics


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.

like image 1
tjw Avatar answered Oct 24 '22 06:10

tjw