Support NSDocument changes in an external editor?
Asked Answered
B

4

14

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.

Bise answered 10/2, 2014 at 22:11 Comment(6)
Would FSEvents work?Ledda
possible duplicate of Notifications for file system changed?Cistercian
Looking for something built into NSDocument. Someone mentioned presentedItemDidChange in NSFilePresenter (which NSDocument conforms to). Looking to see if there's something that shows the UI to ask the user what to do as well though.Bise
According to the documentation, NSDocument should do this for you — look at the docs for - (NSDate *)fileModificationDateGaudet
@TonyArnold That would require polling, which doesn't seem like a solution to me.Raye
@Raye — according to the documentation, the alerts should be presented to the user without any need for polling. I believe Sam has checked though, and it doesn't seem to work as described in the docs.Gaudet
C
4

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.
Crack answered 11/8, 2017 at 13:0 Comment(0)
N
1

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

Potential duplicate of this question.

Napolitano answered 10/2, 2014 at 22:17 Comment(1)
Thanks! It's a bit different than the other question. I'm only interested in the document's content changing. I was hoping for something built-in to NSDocument.Bise
C
1

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
]
Cofferdam answered 27/11, 2014 at 15:45 Comment(0)
O
0

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.

Onceover answered 11/3, 2014 at 11:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.