Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Support NSDocument changes in an external editor?

I have an NSDocument with some simple code:

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError {
  self.string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
  return YES;
}

If I change the file in an external editor, how do I get notified of this so I can handle it? I assume there is something built in for this, but I can't find it.

I'm looking for something built into NSDocument. I'm aware of FSEvent, but that seems too low level to do something very common for most document-based apps.

like image 766
Sam Soffes Avatar asked Feb 10 '14 22:02

Sam Soffes


3 Answers

Since OS X v10.7, NSDocument provides a far simpler mechanism you can override in subclasses: -presentedItemDidChange.

Handling -presentedItemDidChange, Ignoring Metadata Changes

Just relying on this callback can produce false positives, though, when metadata change. That got on my nerves quickly for files stored in Dropbox, for example.

My approach to deal with this in general, in Swift, is like this:

class MyDocument: NSDocument {
    // ...

    var canonicalModificationDate: Date!

    override func presentedItemDidChange() {

        guard fileContentsDidChange() else { return }

        guard isDocumentEdited else {
            DispatchQueue.main.async { self.reloadFromFile() }
            return
        }

        DispatchQueue.main.async { self.showReloadDialog() }
    }

    fileprivate func showReloadDialog() {

        // present alert "do you want to replace your stuff?"
    }

    /// - returns: `true` if the contents did change, not just the metadata.
    fileprivate func fileContentsDidChange() -> Bool {

        guard let fileModificationDate = fileModificationDateOnDisk()
            else { return false }

        return fileModificationDate > canonicalModificationDate
    }

    fileprivate func fileModificationDateOnDisk() -> Date? {

        guard let fileURL = self.fileURL else { return nil }

        let fileManager = FileManager.default
        return fileManager.fileModificationDate(fileURL: fileURL)
    }
}

Now you have to update the canonicalModificationDate in your subclass, too:

  • In a callback from the "do you want to replace contents?" alert which I call -ignoreLatestFileChanges so you don't nag your user ad infitium;
  • In -readFromURL:ofType:error: or however you end up reading in contents for the initial value;
  • In -dataOfType:error: or however you produce contents to write to disk.
like image 154
ctietze Avatar answered Nov 19 '22 02:11

ctietze


You want to register with the FSEvents API. Since 10.7, you can watch arbitrary files.

Potential duplicate of this question.

like image 42
bitgarden Avatar answered Nov 19 '22 03:11

bitgarden


When I open a document in my document-based app, edit in in another application, and switch back to my app, the same method that you mentioned (readFromData:ofType:error:) is called with the new data. This method is called when you restore a previous version from the Versions browser, too.

You could then add a boolean instance variable to check whether it's being called because of an external update (in my case, I check whether one of my IBOutlets is initialized: if it's not, the document is being loaded for the first time). You might want to move your code that makes use of the string instance variable into some method that you can call if the document is already initialized, like this:

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError {
    self.string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    if (self.isLoaded)
        [self documentChanged];
    return YES;
}

- (void)windowControllerDidLoadNib:(FCWindowController *)windowController {
    self.isLoaded = YES;
    [self documentChanged];
}

- (void)documentChanged {
    // use self.string as you like
]
like image 1
Nickkk Avatar answered Nov 19 '22 03:11

Nickkk