Question

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.

Was it helpful?

Solution

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.

OTHER TIPS

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

Potential duplicate of this question.

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
]

NSMetadataQuery seems to be the best way to monitor file and folder changes without polling and with a low cpu overhead.

Some basic code for watching a folder, you'd just want to set the filePattern to the filename and not the wildcard *

NSString* filePattern = [NSString stringWithFormat:@"*"];
NSString *watchedFolder = @"not/fake/path";

NSMetadataQuery *query = [[NSMetadataQuery alloc] init];
[query setSearchScopes:@[watchedFolder]];
NSString *itemName = (NSString*)kMDItemFSName;

[query setPredicate:[NSPredicate predicateWithFormat:@"%K LIKE %@", NSMetadataItemDisplayNameKey, filePattern]];

NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(queryFoundStuff:) name:NSMetadataQueryDidFinishGatheringNotification object:query];
[nc addObserver:self selector:@selector(queryFoundStuff:) name:NSMetadataQueryDidUpdateNotification object:query];
[query setNotificationBatchingInterval:0.5];
[query startQuery];

- (void)queryFoundStuff:(NSNotification *)notification {
    [query disableUpdates];
    NSLog(@"Notification: %@", notification.name);
    NSMutableArray *results = [NSMutableArray arrayWithCapacity:query.resultCount];

    for (NSUInteger i=0; i<query.resultCount; i++) {
      [results addObject:[[query resultAtIndex:i] valueForAttribute:NSMetadataItemPathKey]];
    }

    // file has updated, do something 

    [query enableUpdates];
}

I've never been able to find an ideal solution to watching files for updates, NSFilePresenter sounds like it should be the appropriate high level solution, but from what I can tell it only works if the file is being edited by another App using NSFilePresenter also. I've also tried VDKQueue and SCEvents which wrap low level kernel events but have a cpu overhead.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top