Reputation: 421
I've added iCloud into my SwiftUI app and everything seems to be working great, however I need to implement an on off toggle for it. After searching, I found a couple forum posts that suggested to re-create the container when icloud is toggled on off. Here's the code:
lazy var persistentContainer: NSPersistentContainer = {
return setupContainer()
}()
/* This is called when the iCloud setting is turned on and off */
func refreshCoreDataContainer() {
/* Save changes before reloading */
try? self.persistentContainer.viewContext.save()
/* Reload the container */
self.persistentContainer = self.setupContainer()
}
private func setupContainer() -> NSPersistentContainer {
let useCloudSync = UserSettings.shared.enableiCloudSync
let container: NSPersistentContainer!
/* Use the icloud container if the user enables icloud, otherwise use the regular container */
if useCloudSync {
container = NSPersistentCloudKitContainer(name: "App")
} else {
container = NSPersistentContainer(name: "App")
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
container.persistentStoreDescriptions.first?.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
/* Load the data */
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
container.viewContext.automaticallyMergesChangesFromParent = true
})
return container
}
The problem is that once I reload the container, the app crashes with this error:
Thread 1: "executeFetchRequest:error: A fetch request must have an entity."
Multiple NSEntityDescriptions claim the NSManagedObject subclass 'App.ColorCollection' so +entity is unable to disambiguate.
'ColorCollection' (0x60f000022000) from NSManagedObjectModel (0x607000067260) claims 'App.ColorCollection'.
`
I think the crash has to do with SwiftUI keeping a reference to the old container. When the window is created it is passing the container to it using the enviroment:
let contentView = MyContentView().environment(\.managedObjectContext, persistentContainer.viewContext)
So I tried to close the window, reload the container, then re create the window below, but the app still crashes.
func refreshCoreDataContainer() {
windowController.window?.close()
/* Save changes before continuing */
try? self.persistentContainer.viewContext.save()
self.persistentContainer = self.setupContainer()
self.createAndShowMainWindow()
}
How do I implement a iCloud toggle in SwiftUI without it crashing?
Upvotes: 1
Views: 582
Reputation: 152
SwiftUI 2.0 & IOS 14.3
I'm going through this exact problem right now. Here's the answer I've puzzled together using these two resources:
Using Core Data with SwiftUI 2.0 and Xcode 12
CoreData+CloudKit | On/off iCloud sync toggle
If anyone comes across a better answer please let me know!
final class PersistentContainer {
private static var _model: NSManagedObjectModel?
private static func model(name: String) throws -> NSManagedObjectModel {
if _model == nil {
_model = try loadModel(name: name, bundle: Bundle.main)
}
return _model!
}
private static func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
throw CoreDataModelError.modelURLNotFound(forResourceName: name)
}
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
throw CoreDataModelError.modelLoadingFailed(forURL: modelURL)
}
return model
}
enum CoreDataModelError: Error {
case modelURLNotFound(forResourceName: String)
case modelLoadingFailed(forURL: URL)
}
public static func getContainer(iCloud: Bool) throws -> NSPersistentContainer {
let name = "YOUR APP" // Put your model name here
if iCloud {
return NSPersistentCloudKitContainer(name: name, managedObjectModel: try model(name: name))
} else {
return NSPersistentContainer(name: name, managedObjectModel: try model(name: name))
}
}
}
class CoreDataManager {
static let shared = CoreDataManager()
lazy var persistentContainer: NSPersistentContainer = {
setupContainer()
}()
func refreshCoreDataContainer() {
try? self.persistentContainer.viewContext.save()
self.persistentContainer = self.setupContainer()
}
private func setupContainer() -> NSPersistentContainer {
let iCloud = UserDefaults.standard.bool(forKey: "iCloudOn") // Replace with your UserDefaults boolean here
do {
let newContainer = try PersistentContainer.getContainer(iCloud: iCloud)
guard let description = newContainer.persistentStoreDescriptions.first else { fatalError("No description found") }
if iCloud {
newContainer.viewContext.automaticallyMergesChangesFromParent = true
newContainer.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
} else {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
newContainer.loadPersistentStores { (storeDescription, error) in
if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") }
}
return newContainer
} catch {
print(error)
}
fatalError("Could not setup Container")
}
}
Then to use just put this in your App.swift
file:
let persistenceController = CoreDataManager.shared
And this within the App.swift
var body
.environment(\.managedObjectContext, persistenceController.persistentContainer.viewContext)
To use put this on your toggle
.onChange(of: toggleVariable) { value in
CoreDataManager.shared.refreshCoreDataContainer()
}
Upvotes: 1