Problem getting NSPersistentCloudKitContainer to sync between devices (SwiftUI, iOS) - core-data

I have set up my Core Data model with NSPersistentCloudKitContainer:
struct PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "LearningCloudKit")
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)")
}
})
}
}
That's pretty much the standard stuff that Xcode presents when I create a new project with CoreData and CloudKit.
The view presenting the data is fed by
#Environment(\.managedObjectContext) private var viewContext
#FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: false)],
animation: .default)
private var items: FetchedResults<Item>
Sync between my devices works somewhat but when I enter new data into one device it takes more than 15 minutes to show up on the other. Don't know, some times it doesn't arrive until I force close the app and restart. And sometimes I have to enter data on one device and force close to see data from other device.
I have enabled remote notifications, I have enabled CloudKit and my Container. I have checked with CloudKit dashboard that the changes are uploaded into the cloud pretty fast. It seems that they aren't downloaded to the devices so quickly.
What else do I have to do to get this working? Like enter data in one device and in less than a minute it appears on the other device. A background service maybe?

OMG. I found the answer myself. In PersistenceController at the end I added:
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.automaticallyMergesChangesFromParent = true
Credit: schwiftyui.com

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

How do I get SwiftUI to work with Core Data (after starting a project)?

I get this error when trying to get Xcode working with Core Data. Any help would be very appreciated.
Context in environment is not connected to a persistent store coordinator: <NSManagedObjectContext: 0x6000008a8820>
After commenting out all the other environments so that the persistence envinronment is only used in the top level, I still have the same error.
Here is the Apps Top-Level Swift File:
import SwiftUI
import CoreData
#main
struct Draw_DailyApp: App {
let persistenceController = PersistenceController.shared
#Environment(\.scenePhase) var scenePhase
#FetchRequest(entity: Drawing.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Drawing.img, ascending: true)])
var orders: FetchedResults<Drawing>
#State var showOrderSheet = false
// Creating a global environment and placing it into the environment
#ObservedObject var searchObjectController: SearchObjectController = SearchObjectController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
.environmentObject(self.searchObjectController)
}
.onChange(of: scenePhase) { _ in
persistenceController.save()
}
}
}
and here is the file where persistent controller is used (Core Data xcdatamodeld has one Binary Data attribute called img)
Persistence.Swift
import CoreData
import UIKit
struct PersistenceController {
static let shared = PersistenceController()
func save() {
let context = container.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
// Show some error here
}
}
}
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
for _ in 0..<10 {
let newItem = Drawing(context: viewContext)
newItem.img = Data()
newItem.date = "02081999"
}
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: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "DrawDaily")
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)")
}
})
}
}
PS: I didn't start this project with Core Data, I added it in later
Your #FetchRequest doesn't have a NSManagedObjectContext upon init. You are creating it with this line
let persistenceController = PersistenceController.shared
Your #FetchRequest has to move down into the ContentView for it to work.
This line has to be called
.environment(\.managedObjectContext, persistenceController.container.viewContext)
when initializing the View that contains the #FetchRequest

Core Data Widget with shared container conflict

The Scenario
I'm using Core Data inside my main app and my widget.
To share my Persistent Container I use this custom stack that targets both my app and my widget and its working fine
class CoreDataService {
static let shared = CoreDataService()
lazy var context: NSManagedObjectContext = persistentContainer.viewContext
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "my.app.name")
let storeURL = URL.storeURL(for: "my.app.group", databaseName: "my.app.name")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
container.persistentStoreDescriptions = [storeDescription]
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
func saveContext () {
let context = self.context
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
The Problem
But when I access my widget from the home and I tap it to go to the app, whenever I use #FetchRequest in my code I get this error
CoreData: error: +[Subject entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
And then my app crashes with this Stack Trace
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'executeFetchRequest:error: A fetch request must have an entity.'
Workaround
I partially solved it by changing this FetchRequest
#FetchRequest(entity: Subject.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Subject.name, ascending: true)]) private var subjects: FetchedResults<Subject>
to this one
#FetchRequest(fetchRequest: NSFetchRequest(entityName: "Subject")) private var subjects: FetchedResults<Subject>
But still don't know what I did wrong in the first place and why is this stupid change solving my problem and I would like to know if there is another way to solve it.

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

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

Resources