Слияние изменений 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);
}