简体   繁体   中英

CoreData + Cloudkit fetch() after app uninstall/reinstall returns no results

I am attempting to configure CoreData+CloudKit using NSPersistentCloudKitContainer to automatically sync data to/from CloudKit.

Following the Apple guide , it was trivial enough to set up an Entity, add the appropriate capabilities in Xcode, set up the persistentContainer and saveContext in my AppDelegate.

I'm making fetch() and save() calls through the NSManagedObjectContext , and I'm able to save and fetch records without issue. I can see them in the CloudKit dashboard.

However, if I uninstall the app from the simulator and rebuild/reinstall, my fetchRequest (which has no NSPredicate or sorting, just fetching all records) is always returning an empty list. I'm using the same iCloud account, and I've tried both the public and private database scope. If I create a new record and then retry my fetch request I can retrieve that newly created record, but never any of the old records. I'm 100% certain these records are still in the CloudKit database, as I can see them on the CloudKit Dashboard web app.

I took a look at Apple's CoreDataCloudKitDemo app, and it is able to fetch "Post" entities from the CloudKit database after an uninstall/reinstall, so I know it is possible. However, it is using an NSFetchedResultsController , which won't work for my application (mine is a SpriteKit game).

I attempted copying my CoreData+Cloudkit code into a brand new Xcode project and I can reproduce this issue there. Here's my code for reference:

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)")
        }
    }
}


What am I missing? Why can I only fetch the records created during this app's install and not prior ones?

UPDATE : It seems that if I wait for a few seconds and re-try my fetch on the first app install that I am able to retrieve the results from the CloudKit database. I can also see a vast number of CoreData+CloudKit log messages in the console upon first launch. Here's what I'm thinking -- even when using NSPersistentCloudKitContainer , a fetch() is reading/writing to the local CoreData store, and then a separate process is running in the background to mirror and merge the local CoreData records with the CloudKit records.

As such, I believe I need to somehow wait/be notified that this sync/merge of local CoreData and CloudKit records has completed on first launch before making my fetch() call, rather than making the fetch() call immediately as the app opens. Any ideas?

You need to use NSFetchedResultsController , why do you think it wouldn't work for your application?

The reason it is necessary is NSFetchedResultsController monitors the viewContext and when the sync process downloads new objects and inserts them into a background context the automaticallyMergesChangesFromParent merges the objects in to the viewContext and advances the generation token. The FRC's delegate methods are called to notify you if objects are inserted, updated or deleted from the fetched objects array which are objects in the context that match the fetch request entity and predicate.

What you can try is this:

  • Set NSPersistentCloudKitContainerOptions
let id = "iCloud.yourid"  
let options = NSPersistentCloudKitContainerOptions(containerIdentifier: id)  
description?.cloudKitContainerOptions = options
  • Initialize your CloudKit schema (this is required at least once)
do {
     try container.initializeCloudKitSchema()
  } catch {
    print("ERROR \(error)")
 }

Edit:
Can you change lazy var persistentContainer: NSPersistentContainer

to lazy var persistentContainer: NSPersistentCloudKitContainer

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM