NSPersistentClouKitContainer creates CD_CKRecords but does not export values - core-data

I am converting existing app from NSPersistentContainer to NSPersistentCloudKitContainer
AppDelegate code:
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name:"GridModel")
// enable history tracking and remote notifications
guard let publicStoreDescription = container.persistentStoreDescriptions.first else {
fatalError("###\(#function): failed to retrieve a persistent store description.")
}
// public
let publicStoreUrl = publicStoreDescription.url!.deletingLastPathComponent().appendingPathComponent("GridModel-public.sqlite")
publicStoreDescription.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
publicStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
let containerIdentifier = publicStoreDescription.cloudKitContainerOptions!.containerIdentifier
let publicStoreOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: containerIdentifier)
if #available(iOS 14.0, *) {
publicStoreOptions.databaseScope = CKDatabaseScope.public
} else {
// Fallback on earlier versions???
}
publicStoreDescription.cloudKitContainerOptions = publicStoreOptions
print(containerIdentifier)
container.loadPersistentStores(completionHandler: { (loadedStoreDescription, error) in
if let loadError = error as NSError? {
fatalError("###\(#function): Failed to load persistent stores:\(loadError)")
} else if let cloudKitContainerOptions = loadedStoreDescription.cloudKitContainerOptions {
if #available(iOS 14.0, *) {
if .public == loadedStoreDescription.cloudKitContainerOptions?.databaseScope {
self._publicPersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
} else if .private == loadedStoreDescription.cloudKitContainerOptions?.databaseScope {
self._privatePersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
} else if .shared == cloudKitContainerOptions.databaseScope {
self._sharedPersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
}
} else {
// Fallback on earlier versions
}
} /*else if appDelegate.testingEnabled {
if loadedStoreDescription.url!.lastPathComponent.hasSuffix("private.sqlite") {
self._privatePersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
} else if loadedStoreDescription.url!.lastPathComponent.hasSuffix("shared.sqlite") {
self._sharedPersistentStore = container.persistentStoreCoordinator.persistentStore(for: loadedStoreDescription.url!)
}
}*/
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
// 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)")
}
#if DEBUG
do {
// Use the container to initialize the development schema.
try container.initializeCloudKitSchema(options: [])
} catch {
// Handle any errors.
fatalError("###\(#function): failed to load persistent stores: \(error)")
}
#endif
// Observe Core Data remote change notifications.
NotificationCenter.default.addObserver(self,
selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
return container
}()
The very first time I run, Creates all the corresponding CD CKRecords in CloudKit public database _defaultZone. All values are copied from Core Data to CloudKit.
When the app is actively running I deleted a Core Data record and added a new record it did not delete it from Cloud Kit. Also when I add a new record in Core Data it did not export and add to Cloud Kit. When the app is actively running any changes I make are not exported and updated in Cloud Kit. What changes should I make to the code?
But when I run the app again then all the previous changes that I made when the app was active are updated. So looks like it does not perform live updates until the app is rerun again.
I get another error on the Debug Console "Custom zones are not allowed in public DB". I am not writing to custom DB. I wonder if this is preventing active updates?
oreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _requestAbortedNotInitialized:](1983): <NSCloudKitMirroringDelegate: 0x2824d16c0> - Never successfully initialized and cannot execute request '<NSCloudKitMirroringExportRequest: 0x280a84780> FDB61E8D-658F-4C77-8615-EF07BAB72BAD' due to error: <CKError 0x28119ba20: "Partial Failure" (2/1011); "Failed to modify some records"; uuid = 6C66A151-DB59-4A1B-B3BD-02CFB9A4139C; container ID = "iCloud.com.greenendpoint.nr2r"; partial errors: {
B-63B7-45D2-BFE0-599972FB6FD7:(com.apple.coredata.cloudkit.zone:defaultOwner) = <CKError 0x2810e44e0: "Server Rejected Request" (15/2027); server message = "Custom zones are not allowed in public DB"; op = 23AFDAC55F70DC8D; uuid = 6C66A151-DB59-4A1B-B3BD-02CFB9A4139C>
Please help!

It worked. I had to delete my app on the test device and restart again. Looks like it recreated the data store and synced with Cloud Kit.

Related

Core Data- Using Multiple Merge Policies

I am importing a large amount of data and persisting it via CoreData, setting constraints to ensure no data is duplicated (e.g. if it is imported multiple times). I do this via batch insert and use the merge policy NSMergeByPropertyStoreTrumpMergePolicy because any existing data in the store should not be overwritten upon import.
Later, the user can trigger some actions that require another batch insert, but with the merge policy NSMergeByPropertyObjectTrumpMergePolicy because now I want any changed attributes to have the new values persisted to the store.
This is all just as I need, except... based on my experimentation, there is no way to effectively CHANGE the merge policy once you have set it.
Once we make an insert using NSMergeByPropertyStoreTrumpMergePolicy, even if we now update context.mergePolicy to NSMergeByPropertyObjectTrumpMergePolicy, this will have no effect. Core Data will continue to generate SQL for the prior merge policy instead. Restarting the app and running the two steps in the opposite order verifies the same behavior -- whichever merge policy you specify first will become "stuck" in Core Data's behavior, even if you later update context.mergePolicy again in your code.
In fact, I found that even if I set two different merge policies on two different contexts (e.g. viewContext and backgroundContext) -- the behavior still gets "stuck". Whichever merge policy you use first will become the merge policy Core Data is using to generate SQL requests, even for the other context!
Can anybody verify if they have run into this and thoughts on workarounds?
I have found one thing -- which is to go all the way up the stack and create a second PersistenceController (and with it, NSPersistentContainer), using one for each merge policy. This works, but seems like overkill / not as intended by Apple?
Here is a simple SwiftUI-based project for testing.
You just need a CoreData model with an entity named Item, and two String attributes: col1 and col2. Set col1 as a constraint on Item.
And I recommend adding the launch argument to your scheme -com.apple.CoreData.SQLDebug 1 so you can see the SQL that Core Data is generating. More info on that here: https://useyourloaf.com/blog/debugging-core-data/
import SwiftUI
import CoreData
class Model: ObservableObject {
var dataArray = [[String:Any]]()
}
struct ContentView: View {
#State var model: Model = Model()
var body: some View {
VStack {
Button("Insert Data - PropertyStoreTrump") {
insertData()
}
Button("Insert More Data - PropertyObjectTrump") {
insertMoreData()
}
Spacer()
Button("Fetch Data") {
fetchAllData()
}
Button("Clear Data") {
clearAllData()
}
}
}
// Insert data using NSMergeByPropertyStoreTrumpMergePolicy
private func insertData() {
model.dataArray = [[String:Any]]()
model.dataArray.append(["col1":"1", "col2":"1"])
model.dataArray.append(["col1":"2", "col2":"1"])
model.dataArray.append(["col1":"3", "col2":"1"])
batchInsert(context: PersistenceController.shared.container.viewContext, mergePolicy: NSMergeByPropertyStoreTrumpMergePolicy)
}
// Insert data using NSMergeByPropertyObjectTrumpMergePolicy
private func insertMoreData() {
model.dataArray = [[String:Any]]()
model.dataArray.append(["col1":"2", "col2":"0"])
model.dataArray.append(["col1":"3", "col2":"0"])
model.dataArray.append(["col1":"4", "col2":"0"])
batchInsert(context: PersistenceController.shared.container.newBackgroundContext(), mergePolicy: NSMergeByPropertyObjectTrumpMergePolicy)
}
private func batchInsert(context: NSManagedObjectContext, mergePolicy: AnyObject) {
context.mergePolicy = mergePolicy
context.perform {
let insertRequest = NSBatchInsertRequest(entity: Item.entity(), objects: model.dataArray)
do {
let result = try context.execute(insertRequest)
} catch {
print("batch insert error: \(error)")
}
}
}
private func fetchAllData() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Item")
do {
let results = try PersistenceController.shared.container.viewContext.fetch(fetchRequest) as! [Item]
for result in results {
print(" \(result.col1 ?? "") \(result.col2 ?? "")")
}
} catch {
print("error with fetch: \(error)")
}
}
private func clearAllData() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "Item")
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
do {
try PersistenceController.shared.container.viewContext.execute(batchDeleteRequest)
} catch {
print("error with delete request: \(error)")
}
}
}
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "CoreDataStackOverflow")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
The workaround I found is to add this line to PersistenceController
static let shared2 = PersistenceController()
then use PersistenceController.shared.container... for one merge policy
and PersistenceController.shared2.container... for the other

SwiftUI CloudKit Public Database with NSPersistentCloudKitContainer

Based on WWDC20 talk bellow:
https://developer.apple.com/videos/play/wwdc2020/10650/
The way to setup CloudKit Public Database with NSPersistentCloudKitContainer in "one line of code" is this:
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey:NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.cloudKitContainerOptions?.databaseScope = .public
How would that be on the new SwiftUI Persistent.swift template?
I tried the code bellow but didn't work:
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
}
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
//This doesnt work
//container.cloudKitContainerOptions?.databaseScope = .public
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "Market")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
guard let description = container.persistentStoreDescriptions.first else {
print("Can't set description")
fatalError("Error")
}
description.cloudKitContainerOptions?.databaseScope = .public

Multiple duplicate NSPersistentStoreRemoteChange notifications fired from CloudKit + CoreData

I am seeing .NSPersistentStoreRemoteChange notifications being received multiple times, sometimes up to 10 times.
I don't think this is harmful, but best case it's using up processing power.
So my questions are:
Is this harmful?
Can I prevent it?
If not, is there a recommended way to ignore duplicate notifications?
--
I have the following code to setup my container. This is contained in the initialiser of a singleton and I have confirmed that it is called once.
guard let modelURL = Bundle(for: type(of: self)).url(forResource: momdName, withExtension:"momd"),
let mom = NSManagedObjectModel(contentsOf: modelURL)
else {
fatalError("🔐 Error loading model from bundle")
}
let containerURL = folderToStoreDatabaseIn.appendingPathComponent("Model.sqlite")
container = NSPersistentCloudKitContainer(name: momdName, managedObjectModel: mom)
guard let description = container.persistentStoreDescriptions.first else {
fatalError("🔐 ###\(#function): Failed to retrieve a persistent store description.")
}
description.url = containerURL
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
super.init()
// this must be called after super.init()
// ***** ADD OBSERVER *****
NotificationCenter.default.addObserver(self,
selector: #selector(updatedFromCKCD(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
if let tokenData = try? Data(contentsOf: tokenFile) {
do {
lastToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
Logger.error("🔐 ###\(#function): Failed to unarchive NSPersistentHistoryToken. Error = \(error)")
}
}
The code to process these changes:
// https://developer.apple.com/documentation/coredata/consuming_relevant_store_changes
#objc func updatedFromCKCD(_ notifiction: Notification) {
let fetchHistoryRequest = NSPersistentHistoryChangeRequest.fetchHistory(
after: lastToken
)
let context = container.newBackgroundContext()
guard
let historyResult = try? context.execute(fetchHistoryRequest)
as? NSPersistentHistoryResult,
let history = historyResult.result as? [NSPersistentHistoryTransaction]
else {
Logger.error("⛈ Could not convert history result to transactions")
assertionFailure()
return
}
Logger.debug("⛈ Found cloud changes since: \(self.lastToken?.description ?? "nil")")
DispatchQueue.main.async {
// look for particular set of changes which require the user to take action
...
}
}

Allow ComplicationController to access CoreData SwiftUI

In Xcode 12 beta 2 using SwiftUI I want my ComplicationController to make a FetchRequest in order to update my complications but I am having trouble injecting the persistent store into the environment.
I should note this is a pure SwiftUI App in watchOS where the app entry point is the #main struct. There is no ExtensionDelegate or HostingController.
For the watchOS app itself this is how I'm setting up the PersistentContainer:
struct RunPlanner: App {
#Environment(\.scenePhase) private var scenePhase
#StateObject private var persistentStore = PersistentStore.shared
#ObservedObject var selection = TabSelection()
var body: some Scene {
WindowGroup {
TabView(selection: $selection.currentTab) {
WatchAddRunView(tabSelection: selection)
.tag(0)
ContentView()
.tag(1)
}
.environment(\.managedObjectContext, persistentStore.context)
.animation(.easeIn)
}
.onChange(of: scenePhase) { phase in
switch phase {
case .active:
print("\(#function) REPORTS - App change of scenePhase to ACTIVE")
case .inactive:
print("\(#function) REPORTS - App change of scenePhase to INACTIVE")
case .background:
print("\(#function) REPORTS - App change of scenePhase to BACKGROUND")
savePersistentStore()
default:
print("\(#function) REPORTS - App change of scenePhase Default")
}
}
}
func savePersistentStore() {
persistentStore.saveContext()
}
}
This works for the app itself to save values to CoreData however my ComplicationController is not seeing the NSPersistentStoreContainer and I'm not sure how to inject it.
My current attempt within my ComplicationController class is this:
class ComplicationController: NSObject, CLKComplicationDataSource {
#Environment(\.managedObjectContext) var moc
let request = NSFetchRequest<RunEvents>(entityName: "RunEvents")
....complication code...
}
func getSavedRunName() -> String {
var activeName = "Run Roster"
do {
let savedRuns = try moc.fetch(request)
savedRuns.forEach({
if $0.isActive {
guard let fetchedName = $0.name else { return }
activeName = fetchedName
}
})
} catch {
print("Error in Fetch Request")
}
return activeName
}
However the getSavedRunName method will cause the app to crash on execution with the debugger saying "reason: '+entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'RunEvents''"
I've searched and fumbled around for various solutions with no positive results. Any insight here is very much appreciated.
-Dan
The CLKComplicationDataSource is a separate separate executable from the WKExtensionDelegate, so you need to set up your CoreData stack in each. Additionally, you use an App Group to share the same Core Data files. I use an extension like this from all my targets.
extension NSPersistentContainer {
static func appContainer() -> NSPersistentContainer {
let container = NSPersistentContainer(name: AppConstants.databaseName)
let storeURL = URL.storeURL(for: AppConstants.appGroupName, databaseName: AppConstants.databaseName)
let storeDescription = NSPersistentStoreDescription(url: storeURL)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}
}
Note that there would be additional work TBD to sync changes made in extensions back into the app's contexts. My complications (and widgets) are read-only, and the OS runs them on demand, so they initialize fresh and up-to-date.
When syncing across devices, I use CloudCore

How to load a new Core Data file in the shared container?

I was working with NSPersistentCloudKitContainer using the Apple sample code, and the setup is very simple like this:
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Name")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
But now I have a need to change to use a database that is in the shared container so that it can be used with extension.
I tried to change it to this code
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Name")
// Create a store description for a CloudKit-backed local store
let storeURL = URL.storeURL(for: "group.com.Name", databaseName: "Name")
let cloudStoreDescription =
NSPersistentStoreDescription(url: storeURL)
cloudStoreDescription.configuration = "Default"
// Set the container options on the cloud store
cloudStoreDescription.cloudKitContainerOptions =
NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.Name")
// Update the container's list of store descriptions
container.persistentStoreDescriptions = [cloudStoreDescription]
// Load both stores
container.loadPersistentStores { storeDescription, error in
guard error == nil else {
fatalError("Could not load persistent stores. \(error!)")
}
}
return container
}()
public extension URL {
/// Returns a URL for the given app group and database pointing to the sqlite database.
static func storeURL(for appGroup: String, databaseName: String) -> URL {
guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
fatalError("Shared file container could not be created.")
}
return fileContainer.appendingPathComponent("\(databaseName).sqlite")
}
}
but I got this error
Fatal error: Could not load persistent stores. Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=Unable to find a configuration named 'Default' in the specified managed object model.}:

Resources