Слияние изменений UIDocument для конфликтов iCloud

Я потратил несколько дней, пытаясь найти или выяснить для себя, как программно объединить изменения UIDocument, когда срабатывает уведомление UIDocumentStateChangedNotification и в состоянии документа установлен UIDocumentStateInConflict.

Все примеры, которые я могу найти ("Яблоки", "Рэй Вендерлих" и т. Д.), Подробно описывают подсказку пользователю выбрать метод версии. Я не могу найти ни одного, который продемонстрировал бы правильный способ программного слияния. Это беспокоит меня, так как заставляет меня думать, что доверять слишком странно и, как правило, его избегают в качестве решения? Мой опыт с этим до сих пор укрепляет эту позицию.

Позвольте мне подробно описать каждую проблемную область в моих попытках.

1) Как правильно читать текущее содержимое документа и версии конфликта NSFileVersion с целью слияния? Использование чего-либо с блоком завершения очень грязно при синхронизации. UIDocument 's openWithCompletionHandler: не заманчиво использовать. На самом деле, как правило, каков рекомендуемый способ только для чтения UIDocument? Зачем открывать документ только для чтения? Я попытался использовать readFromURL UIDocument: это хорошо для текущего документа, но если я пытаюсь использовать его в любой из конфликтных версий NSFileVersion, он читает текущую версию, а не версию по URL (я использовал Терминал MacOS, чтобы копаться в файлы../data/.DocumentRevisions-V100/PerUID/..., чтобы подтвердить это.). Для конфликтных версий единственный способ, которым он работает для меня - это прямой доступ к этим файлам. (например, NSData initWithContentsOfFile:)

2) После прочтения вариантов файла и возможности слияния, как правильно сохранить слияние? Этот действительно не зарегистрирован нигде, где я могу найти. Единственный подход, с которым мне удалось, - это повторно использовать один из файлов конфликтов NSFileVersion, перезаписать его, а затем использовать replaceItemAtURL UIDocument: чтобы сделать его текущим. Я также попытался использовать revertToContentsOfURL в UIDocument: после использования replaceItemAtURL: но он просто вылетает без указания причины. Поскольку слияние, кажется, работает без него, я не волнуюсь, но подумал, что я бы включил это как деталь.

3) Симулятор iPhone/iPad (V10.0) не уведомляет о конфликтах, пока я не перезапущу приложение. Это ожидать или я делаю что-то не так? Я спрашиваю, потому что в меню отладки симулятора есть Trigger iCloud Sync, который синхронизирует, но конфликты не помечаются до следующей перезагрузки приложения. Это всего лишь ограничение симулятора?

Спасибо,

2 ответа

Я упростил свой код слияния UIDocument после нескольких недель тестирования и изучения того, что работает, а что нет. Одно из неверных предположений, которые я сделал, заключалось в том, что в UIDocument необходимо включить revertToContentsOfURL: как часть процесса разрешения. Это очень нестабильный вызов API, и его лучше избегать, даже если он используется в @try(), не защищает от ненужных сбоев. Это заставило меня убрать его, просто чтобы посмотреть, что произойдет, и конфликты прояснились без этого. На developer.apple.com был пример кода для разрешения конфликта документов, который подразумевал, что его следует использовать. Похоже, он исчез после WWDC2018.

Единственная оставшаяся проблема заключается в том, что если у вас есть 2 устройства, оба открыты одновременно, вы можете попасть в состояние гонки, так как оба документа объединяются непрерывно.

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

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

- (void) foobar {
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleDocumentStateChange:)
                                                 name:UIDocumentStateChangedNotification
                                               object:_myDocument];
}

- (void) handleDocumentStateChange: (NSNotification *) notification {
    if (_myDocument.documentState & UIDocumentStateInConflict) {
        if (_resolvingConflicts) {
            return;
        }

        NSArray *conflictVersions = [NSFileVersion unresolvedConflictVersionsOfItemAtURL:_myDocument.fileURL];
        if ([conflictVersions count] == 0) {
            return;
        }
        NSMutableArray *docs = [NSMutableArray new];
        [docsData addObject:_myDocument.data]; // Current document data
        _resolvingConflicts = YES;
        for (NSFileVersion *conflictVersion in conflictVersions) {
            MyDocument *myDoc = [[MyDocument alloc] initWithFileURL:conflictVersion.URL];
            NSError *error;
            [myDoc readFromURL:conflictVersion.URL error:&error];
            if ((error == Nil) && (myDoc.data != Nil)) {
                [docs addObject:myDoc.data];
            }
        }

        if ([self mergeDocuments:docs]) {
            [self saveChangesToDocument];
        }

        for (NSFileVersion *fileVersion in conflictVersions) {
            fileVersion.resolved = YES;
        }
        [self deleteiCloudConflictVersionsOfFile:_myDocument.fileURL
                                      completion:^(BOOL success){
                                          self.resolvingConflicts = NO;
                                          dispatch_async(dispatch_get_main_queue(), ^{
                                              // On main thread for UI updates
                                              [[NSNotificationCenter defaultCenter] postNotificationName:kMyDocsUpdateNotification object:nil];
                                          });
                                      }];
    }
}

- (void) deleteiCloudConflictVersionsOfFile : (NSURL *) fileURL {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
        NSFileCoordinator* fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
        [fileCoordinator coordinateWritingItemAtURL:fileURL
                                            options:NSFileCoordinatorWritingForDeleting
                                              error:nil
                                         byAccessor:^(NSURL* writingURL) {
                                             NSError *error;
                                             if ([NSFileVersion removeOtherVersionsOfItemAtURL:writingURL error:&error]) {
                                                 NSLog(@"deleteiCloudConflictVersionsOfFile: success");
                                             } else {
                                                 NSLog(@"deleteiCloudConflictVersionsOfFile: error; %@", [error description]);
                                             }
                                         }];
    });
}

Вот ответ на часть "Зачем открывать документ только для чтения?".

Вам просто нужно убедиться, что чтение "скоординировано", т. Е. Нет конфликтов с файлами, которые уже открыты другим процессом и могут иметь несохраненные изменения.

Вот способ перебрать массив URL-адресов NSDocument и прочитать каждый из них синхронно, т.е. эта процедура не возвращается, пока все файлы не будут прочитаны. Это заставляет любые файлы с несохраненными изменениями сохранять себя, прежде чем произойдет какое-либо чтение.

// NSArray *urls - the urls of UIDocument files you want to read in bulk
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] init];
NSError *error = nil;
[coordinator prepareForReadingItemsAtURLs:urls options:NSFileCoordinatorReadingWithoutChanges writingItemsAtURLs:@[] options:0 error:&error byAccessor:^(void (^ _Nonnull completionHandler)(void)) {
    for (NSURL *url in self->_urls) {
        NSError *error = nil;
        [coordinator coordinateReadingItemAtURL:url options:0 error:&error byAccessor:^(NSURL * _Nonnull newURL) {
            // Read contents of newURL here and process as required
            // ...

        }];
        if (error) {
            NSLog(@"Error reading: %@ %@", url.path, error.localizedDescription);
        }
    }
    completionHandler();
}];
if (error) {
    NSLog(@"Error preparing for read: %@", error.localizedDescription);
}
Другие вопросы по тегам