【问题标题】:CoreData + Cloudkit fetch() after app uninstall/reinstall returns no results应用程序卸载/重新安装后 CoreData + Cloudkit fetch() 不返回任何结果
【发布时间】:2020-12-23 08:26:04
【问题描述】:

我正在尝试使用 NSPersistentCloudKitContainer 配置 CoreData+CloudKit 以自动将数据同步到 CloudKit 或从 CloudKit 同步数据。

the Apple guide 之后,设置实体、在 Xcode 中添加适当的功能、在我的 AppDelegate 中设置 persistentContainersaveContext 已经足够简单了。

我正在通过NSManagedObjectContext 进行fetch()save() 调用,并且我能够毫无问题地保存和获取记录。我可以在 CloudKit 仪表板中看到它们。

但是,如果我从模拟器中卸载应用程序并重新构建/重新安装,我的 fetchRequest(没有 NSPredicate 或排序,只是获取所有记录)总是返回一个空列表。我使用的是同一个 iCloud 帐户,并且我已经尝试过公共和私有数据库范围。如果我创建一条新记录,然后重试我的 fetch 请求,我可以检索新创建的记录,但不会检索任何旧记录。我 100% 确定这些记录仍在 CloudKit 数据库中,因为我可以在 CloudKit Dashboard Web 应用程序上看到它们。

我查看了Apple's CoreDataCloudKitDemo 应用程序,它能够在卸载/重新安装后从 CloudKit 数据库中获取“发布”实体,所以我知道这是可能的。但是,它使用的是 NSFetchedResultsController,这不适用于我的应用程序(我的是 SpriteKit 游戏)。

我尝试将我的 CoreData+Cloudkit 代码复制到一个全新的 Xcode 项目中,我可以在那里重现此问题。这是我的代码供参考:

import UIKit
import CoreData

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    lazy var persistentContainer: NSPersistentContainer = {

        // Create a container that can load CloudKit-backed stores
        let container = NSPersistentCloudKitContainer(name: "coredatacloudkitexample")

        // Enable history tracking and remote notifications
        guard let description = container.persistentStoreDescriptions.first else {
            fatalError("###\(#function): Failed to retrieve a persistent store description.")
        }
        description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
        description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        description.cloudKitContainerOptions?.databaseScope = .public

        container.loadPersistentStores(completionHandler: { (_, error) in
            guard let error = error as NSError? else { return }
            fatalError("###\(#function): Failed to load persistent stores:\(error)")
        })

        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.transactionAuthor = "nibbler"

        // Pin the viewContext to the current generation token and set it to keep itself up to date with local changes.
        container.viewContext.automaticallyMergesChangesFromParent = true
        do {
            try container.viewContext.setQueryGenerationFrom(.current)
        } catch {
            fatalError("###\(#function): Failed to pin viewContext to the current generation:\(error)")
        }

        return container
    }()
}


// ------

import UIKit
import CoreData

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        let viewContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
        let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
        let people: [Person]

        do {
            people = try viewContext.fetch(fetchRequest)
            print("---> fetched People from CoreData: \(people)")

            // people.isEmpty is ALWAYS true (empty array) on first install of app, even if records exist in table in CloudKit
            if people.isEmpty {
                let person = Person(context: viewContext)
                person.name = "nibbler"

                // save the data through managed object context
                do {
                    try viewContext.save()
                    print("--> created Person in CoreData: \(person)")
                } catch {
                    print("---> failed to save Person: \(error.localizedDescription)")
                }
            }
        } catch {
            print("---> error: \(error)")
        }
    }
}


我错过了什么?为什么我只能获取在此应用安装期间创建的记录,而不能获取之前的记录?

更新:似乎如果我等待几秒钟并在第一次安装应用时重新尝试获取,我能够从CloudKit 数据库。首次启动时,我还可以在控制台中看到大量 CoreData+CloudKit 日志消息。这就是我的想法——即使使用NSPersistentCloudKitContainerfetch() 正在读取/写入本地 CoreData 存储,然后在后台运行一个单独的进程来镜像和合并本地 CoreData 记录与 CloudKit记录。

因此,我相信我需要以某种方式等待/收到通知,本地 CoreData 和 CloudKit 记录的同步/合并在首次启动时已完成,然后再拨打我的 fetch() 电话,而不是立即拨打 fetch()应用程序打开。有什么想法吗?

【问题讨论】:

    标签: ios swift core-data cloudkit nspersistentcloudkitcontainer


    【解决方案1】:

    您需要使用NSFetchedResultsController,为什么您认为它不适用于您的应用程序?

    有必要的原因是NSFetchedResultsController 监视viewContext,当同步过程下载新对象并将它们插入后台上下文时,automaticallyMergesChangesFromParent 将对象合并到viewContext 并推进生成令牌.如果从 fetched objects 数组中插入、更新或删除对象,则会调用 FRC 的委托方法来通知您,这些对象是上下文中与 fetch 请求实体和谓词匹配的对象。

    【讨论】:

    • 是的,很好的解决方案。
    • 您可以尝试使用快照,但我个人觉得它还没有准备好。例如,在 performFetch 上,快照更改的委托方法被称为内联,这是非常不寻常的,在很多人的代码中,我看到他们实际上错过了这一点,而是从 fetchedObjects 创建自己的快照,这当然是错误的格式比较到 didChange 提供的快照。 iOS 14 didChange 快照包括我也觉得奇怪的更改的重新加载。请尝试旧的 didChangeContent 并使用 fetchedObjects。然后你可以尝试 didChangeObject 进行精灵增量更新。
    • 是的,didChangeContent 会告诉您 fetchedObjects 何时更新,以及在初始 performFetch 完成后发生更改时。执行另一个 performFetch 的唯一原因是您在 fetchRequest 上编辑谓词或排序描述符。对 fetchedObjects 数组的每次更改、插入、删除等都会调用 didChangeObject。您可以使用它来对您的精灵进行编辑。或者您可以在 didChangeContent 中使用 fetchedObjects 更新整个精灵。
    • 遗憾的是,没有办法创建具有特定记录 ID 的默认对象,以便您可以在应用程序第一次运行时创建它,然后如果从云中下来它可以合并它。相反,您可以尝试 Apple 在他们的示例中使用他们的去重标签例程所做的事情。我预计这会对相关数据造成严重破坏。
    • 我想我找到了一种方法来确定 CloudKit 数据库中是否存在默认对象。如果 CoreData 托管对象上下文没有对象,我将直接使用公共或私有数据库对 CloudKit 进行查询。再次感谢您的所有帮助
    【解决方案2】:

    你可以试试这个:

    • 设置NSPersistentCloudKitContainerOptions
    let id = "iCloud.yourid"  
    let options = NSPersistentCloudKitContainerOptions(containerIdentifier: id)  
    description?.cloudKitContainerOptions = options
    
    • 初始化您的 CloudKit 架构(至少需要一次)
    do {
         try container.initializeCloudKitSchema()
      } catch {
        print("ERROR \(error)")
     }
    

    编辑:
    可以换lazy var persistentContainer: NSPersistentContainer

    lazy var persistentContainer: NSPersistentCloudKitContainer

    【讨论】:

    • 感谢@stamenkovski 的建议!不幸的是,我刚刚尝试了这两种方法并看到相同的行为:( fetch 在首次安装时返回空数组。
    • 我编辑了我的答案。还要确保每次安装应用程序时不要调用 initializeCloudKitSchema。
    • 我按照您的建议尝试了 s/NSPersistentCloudKitContainer/NSPersistentContainer 并观察到相同的行为。我也检查了,因为 NSPersistentCloudKitContainer 是 NSPersistentContainer 的子类,而且这只是编译器的类型提示,我相信它在功能上是等效的。
    • 是的,了解只调用一次initializeCloudKitSchema
    【解决方案3】:

    这是@professormeowingtons 提到的事情,每当您在模拟器上删除应用程序时,它不会显示以前的记录,所以我的建议是在已经配置了 iCloud 帐户的真实设备上尝试您的应用程序,这样您'将能够向您的数据库添加一些记录,然后删除应用程序,重新安装并获取您之前输入的所有记录。

    【讨论】:

    • 我刚刚尝试过这个,在真实设备上看到的行为与我在模拟器上看到的行为相同。第一次安装时的第一次提取仍然返回空数组。
    猜你喜欢
    • 2014-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-23
    • 2016-03-08
    • 2019-02-15
    • 2016-01-15
    相关资源
    最近更新 更多