Поддерживать изменения NSDocument во внешнем редакторе?

У меня есть NSDocument с некоторым простым кодом:

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

Если я изменил файл во внешнем редакторе, как мне получить уведомление об этом, чтобы я мог его обработать? Я предполагаю, что для этого есть что-то, но я не могу его найти.

Я ищу что-то встроенное в NSDocument. Я знаю FSEvent, но это слишком низкий уровень, чтобы сделать что-то очень общее для большинства приложений на основе документов.

Ответ 1

Вы хотите зарегистрироваться в API FSEvents. Начиная с 10.7 вы можете смотреть произвольные файлы.

Потенциальный дубликат этого вопроса.

Ответ 2

Когда я открываю документ в своем приложении на основе документа, редактирую его в другом приложении и переключаюсь обратно в свое приложение, тот же метод, который вы упомянули (readFromData:ofType:error:), вызывается с новыми данными. Этот метод вызывается при восстановлении предыдущей версии из браузера версий.

Затем вы можете добавить переменную экземпляра boolean, чтобы проверить, вызвана ли она из-за внешнего обновления (в моем случае я проверяю, инициализирован ли один из моих IBOutlets: если это не так, документ загружается в первый раз). Возможно, вы захотите переместить свой код, который использует переменную экземпляра string, в какой-то метод, который вы можете вызвать, если документ уже инициализирован, например:

- (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
]

Ответ 3

Так как OS X v10.7, NSDocument обеспечивает гораздо более простой механизм, который вы можете переопределить в подклассах: -presentedItemDidChange.

Обработка -presentedItemDidChange, игнорирование изменений метаданных

Просто полагаясь на этот обратный вызов, вы можете создавать ложные срабатывания, хотя при изменении метаданных. Это ускорилось для файлов, хранящихся в Dropbox, например.

Мой подход, чтобы справиться с этим в целом, в Swift, выглядит следующим образом:

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)
    }
}

Теперь вы также должны обновить canonicalModificationDate в своем подклассе:

  • В обратном вызове из "вы хотите заменить содержимое?" оповещение, которое я называю -ignoreLatestFileChanges, чтобы вы не наворачивали свой пользовательский интерфейс infitium;
  • В -readFromURL:ofType:error: или, тем не менее, вы закончите чтение содержимого для начального значения;
  • В -dataOfType:error: или же вы создаете содержимое для записи на диск.

Ответ 4

NSMetadataQuery, по-видимому, лучший способ отслеживать изменения файлов и папок без опроса и с низкими накладными расходами.

Какой-то базовый код для просмотра папки, вы просто хотите установить filePattern в имя файла, а не подстановочный знак *

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];
}

Я никогда не мог найти идеальное решение для просмотра файлов для обновлений, NSFilePresenter звучит так, как будто это должно быть подходящее решение высокого уровня, но из того, что я могу сказать, работает только в том случае, если файл редактируется другим приложением используя NSFilePresenter. Я также пробовал VDKQueue и SCEvents, которые переносят события ядра низкого уровня, но имеют накладные расходы процессора.