为 iCloud 冲突合并 UIDocument 更改

Merging UIDocument changes for iCloud conflicts

我已经花了几天时间试图找到或自己弄清楚如何在通知 UIDocumentStateChangedNotification[=45 时以编程方式合并 UIDocument 更改=] 触发并且文档的状态设置了 UIDocumentStateInConflict

我能找到的所有示例(Apples、Ray Wenderlich 等)都详细说明了 提示用户使用 select 版本 方法。 我找不到任何可以证明以编程方式合并的正确方法。 这让我很担心,因为它让我觉得它太不稳定而无法信任并且通常被避免作为解决方案? 到目前为止,我在这方面的经验巩固了这一立场。

让我在尝试中详细说明每个有问题的区域。

1) 为合并目的读取当前文档内容和 NSFileVersion 冲突版本的正确方法是什么? 使用带有完成块的任何东西在同步时真的很混乱。 UIDocumentopenWithCompletionHandler: 不太好用。 事实上,作为一项规则,推荐的 UIDocument 只读方式是什么?为什么打开文档只是为了阅读? 我试过使用 UIDocumentreadFromURL: 这对当前文档很好但是如果我尝试在NSFileVersion 中的任何一个 冲突版本它读取当前版本,而不是 URL 的版本(我使用 MacOS 终端深入挖掘 ../data/.DocumentRevisions-V100/PerUID/... 文件以确认这一点。 ). 对于冲突版本,它对我有用的唯一方法是直接读取访问这些文件。 (例如 NSData initWithContentsOfFile:

2)读入文件变体后,成功合并后,如何正确保存合并? 这个在我能找到的任何地方都没有记录。 我成功的唯一方法是重新使用 NSFileVersion 的冲突文件之一,覆盖它,然后使用 UIDocument' s replaceItemAtURL: 使其成为最新的。 在使用 replaceItemAtURL 之后,我还尝试使用 UIDocumentrevertToContentsOfURL: : 但它只是无缘无故地崩溃了。由于合并在没有它的情况下似乎工作正常,所以我并不担心,但我想我会把它作为一个细节包含在内。

3) iPhone/iPad 模拟器 (V10.0) 在我重新启动应用程序之前不会通知冲突。这是可以预料的还是我做错了什么?我问是因为在模拟器的 Debug 菜单下有 Trigger iCloud Sync 可以同步,但直到下一次应用程序重新启动时才会标记冲突。这只是模拟器的限制吗?

谢谢,

这是 "Why open a document just for reading?" 部分的答案。

您只需要确保读取的是 "coordinated",即不与已经由另一个进程打开并且可能有未保存的更改的文件发生冲突。

这是一种遍历 NSDocument url 数组并以同步方式读取每个 url 的方法,即在读取所有文件之前,此例程不会 return。它强制所有具有未保存更改的文件在进行任何读取之前自行保存。

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

经过数周的测试和了解哪些有效,哪些无效,我简化了我的 UIDocument 合并代码。 我做出的错误假设之一是需要包含 UIDocumentrevertToContentsOfURL:作为解决过程的一部分。 这是一个非常不稳定的 API 调用,我发现最好避免,即使在 @try() 中使用它也不能防止不必要的崩溃。 这让我删除它只是为了看看会发生什么,如果没有它,冲突就会很好地解决。 在 developer.apple.com 上,有一个用于解决文档冲突的示例代码,暗示应该使用它。 它似乎已经消失了post-WWDC2018.

剩下的唯一问题是,如果您有 2 台设备同时打开,您可能会进入竞争状态,因为它们会连续合并文档。

早些时候,尽管文档被标记为有冲突,但我曾经历过零冲突版本,但最近我没有看到这种情况发生。一定是我之前做错了什么。我将代码保留在那里,因为它没有害处。

另一个我认为值得一提的问题是,如果您是 UIDocument 的新手,请记住它是 UIKit 的一部分,您需要确保在主线程上完成更新。我发现 this useful tip 解决了一些我仍然遇到的遗留问题。

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