使用 NSPersistentCloudKitContainer 时预填充核心数据存储的最佳方法是什么?

gpi*_*ler 7 core-data swift cloudkit ios13

我遇到以下场景,我从 JSON 文件解析对象并将它们存储到我的核心数据存储中。现在我正在使用NSPersistentCloudKitContainer,当我在不同的设备上运行该应用程序时,它还会解析 JSON 文件并将对象添加到核心数据中。这会导致重复的对象。

现在我想知道是否有:

  • 如果我可以检查实体是否已远程存在,这是一种简单的方法吗?
  • 还有其他方法可以避免对象在 CloudKit 中保存两次吗?
  • 从远程获取数据完成时收到通知吗?

pak*_*aky 9

也许现在回答已经太晚了,但我最近正在研究同样的问题。经过几周的研究,我想在这里留下我所学到的东西,希望能帮助有同样问题的人。

\n
\n

如果我可以检查实体是否已远程存在,这是一种简单的方法吗?

\n
\n
\n

还有其他方法可以避免对象在 CloudKit 中保存两次吗?

\n
\n

是的,我们可以检查该实体是否已存在于 iCloud 上,但这并不是决定是否解析 JSON 文件并将其保存到 CoreData permanentStore 的最佳方法。应用程序可能未连接到 Apple ID / iCloud,或者存在某些网络问题,导致无法可靠地检查该实体是否远程存在。

\n

当前的解决方案是我们自己删除重复数据,方法是向从 JSON 文件添加的每个数据对象添加一个 UUID 字段,并删除具有相同 UUID 的对象。\n大多数时候我还会添加一个 lastUpdate 字段,这样我们就可以保留最新的数据对象。

\n
\n

从远程获取数据完成时收到通知吗?

\n
\n

我们可以添加 NSPersistentStoreRemoteChange 的观察者,并在远程存储发生更改时收到通知。

\n

Apple 提供了一个使用 CoreData 和 CloudKit 的演示项目,并很好地解释了重复数据删除。

\n

将本地存储同步到云\n https://developer.apple.com/documentation/coredata/synchronizing_a_local_store_to_the_cloud

\n

WWDC2019 会议 202:将 CoreData 与 CloudKit 结合使用\n https://developer.apple.com/videos/play/wwdc2019/202

\n

整个想法是监听远程存储中的更改,跟踪更改历史记录,并在有任何新数据传入时对数据进行重复数据删除。(当然我们需要一些字段来确定数据是否重复)。持久存储提供了历史记录跟踪功能,我们可以在这些事务合并到本地存储时获取这些事务,并运行我们的重复数据删除过程。假设我们将在应用程序启动时解析 JSON 并导入标签:

\n
// Use a custom queue to ensure only one process of history handling at the same time\nprivate lazy var historyQueue: OperationQueue = {\n    let queue = OperationQueue()\n    queue.maxConcurrentOperationCount = 1\n    return queue\n}()\n\nlazy var persistentContainer: NSPersistentContainer = {\n    let container = NSPersistentCloudKitContainer(name: "CoreDataCloudKitDemo")\n\n    ...\n    // set the persistentStoreDescription to track history and generate notificaiton (NSPersistentHistoryTrackingKey, NSPersistentStoreRemoteChangeNotificationPostOptionKey)\n    // load the persistentStores\n    // set the mergePolicy of the viewContext\n    ...\n\n    // Observe Core Data remote change notifications.\n    NotificationCenter.default.addObserver(\n        self, selector: #selector(type(of: self).storeRemoteChange(_:)),\n        name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)\n\n    return container\n}()\n\n@objc func storeRemoteChange(_ notification: Notification) {\n    // Process persistent history to merge changes from other coordinators.\n    historyQueue.addOperation {\n        self.processPersistentHistory()\n    }\n}\n\n// To fetch change since last update, deduplicate if any new insert data, and save the updated token\nprivate func processPersistentHistory() {\n    // run in a background context and not blocking the view context.\n    // when background context is saved, it will merge to the view context based on the merge policy\n    let taskContext = persistentContainer.newBackgroundContext()\n    taskContext.performAndWait {\n        // Fetch history received from outside the app since the last token\n        let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest!\n        let request = NSPersistentHistoryChangeRequest.fetchHistory(after: lastHistoryToken)\n        request.fetchRequest = historyFetchRequest\n\n        let result = (try? taskContext.execute(request)) as? NSPersistentHistoryResult\n        guard let transactions = result?.result as? [NSPersistentHistoryTransaction],\n              !transactions.isEmpty\n            else { return }\n\n        // Tags from remote store\n        var newTagObjectIDs = [NSManagedObjectID]()\n        let tagEntityName = Tag.entity().name\n\n        // Append those .insert change in the trasactions that we want to deduplicate\n        for transaction in transactions where transaction.changes != nil {\n            for change in transaction.changes!\n                where change.changedObjectID.entity.name == tagEntityName && change.changeType == .insert {\n                    newTagObjectIDs.append(change.changedObjectID)\n            }\n        }\n\n        if !newTagObjectIDs.isEmpty {\n            deduplicateAndWait(tagObjectIDs: newTagObjectIDs)\n        }\n        \n        // Update the history token using the last transaction.\n        lastHistoryToken = transactions.last!.token\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

在这里,我们保存添加的标签的 ObjectID,以便我们可以在任何其他对象上下文上对它们进行重复数据删除,

\n
private func deduplicateAndWait(tagObjectIDs: [NSManagedObjectID]) {\n    let taskContext = persistentContainer.backgroundContext()\n    \n    // Use performAndWait because each step relies on the sequence. Since historyQueue runs in the background, waiting won\xe2\x80\x99t block the main queue.\n    taskContext.performAndWait {\n        tagObjectIDs.forEach { tagObjectID in\n            self.deduplicate(tagObjectID: tagObjectID, performingContext: taskContext)\n        }\n        // Save the background context to trigger a notification and merge the result into the viewContext.\n        taskContext.save(with: .deduplicate)\n    }\n}\n\nprivate func deduplicate(tagObjectID: NSManagedObjectID, performingContext: NSManagedObjectContext) {\n    // Get tag by the objectID\n    guard let tag = performingContext.object(with: tagObjectID) as? Tag,\n        let tagUUID = tag.uuid else {\n        fatalError("###\\(#function): Failed to retrieve a valid tag with ID: \\(tagObjectID)")\n    }\n\n    // Fetch all tags with the same uuid\n    let fetchRequest: NSFetchRequest<Tag> = Tag.fetchRequest()\n    // Sort by lastUpdate, keep the latest Tag\n    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "lastUpdate", ascending: false)]\n\n    fetchRequest.predicate = NSPredicate(format: "uuid == %@", tagUUID)\n    \n    // Return if there are no duplicates.\n    guard var duplicatedTags = try? performingContext.fetch(fetchRequest), duplicatedTags.count > 1 else {\n        return\n    }\n    // Pick the first tag as the winner.\n    guard let winner = duplicatedTags.first else {\n        fatalError("###\\(#function): Failed to retrieve the first duplicated tag")\n    }\n    duplicatedTags.removeFirst()\n    remove(duplicatedTags: duplicatedTags, winner: winner, performingContext: performingContext)\n}\n
Run Code Online (Sandbox Code Playgroud)\n

最困难的部分(在我看来)是处理被删除的重复对象的关系,假设我们的 Tag 对象与 Category 对象有一对多的关系(每个 Tag 可能有多个 Category)

\n
private func remove(duplicatedTags: [Tag], winner: Tag, performingContext: NSManagedObjectContext) {\n    duplicatedTags.forEach { tag in\n        // delete the tag AFTER we handle the relationship\n        // and be careful that the delete rule will also activate\n        defer { performingContext.delete(tag) }\n        \n        if let categorys = tag.categorys as? Set<Category> {\n            for category in categorys {\n                // re-map those category to the winner Tag, or it will become nil when the duplicated Tag got delete\n                category.ofTag = winner\n            }\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

一件有趣的事情是,如果 Category 对象也是从远程存储中添加的,那么当我们处理关系时它们可能还不存在,但那是另一回事了。

\n