Core Data with App Groups and multiple targets - swift

I know when accessing Core Data across targets I need to use an App Group and common persistentContainer configuration. I'm able to read without issue, and I can create as well.
However, where I'm stuck is when the other target creates and saves an item, the primary app's NSFetchedResultsController doesn't see the change. If I restart the app the data then shows up, so I know it's getting stored properly. I also tried to catch the NSManagedObjectContextDidSave notification in the primary target, but that doesn't seem to do it either.
NotificationCenter.default.addObserver(forName: Notification.Name.NSManagedObjectContextDidSave,
object: nil,
queue: nil) { notification in
print("Got called")
self.managedObjectContext.mergeChanges(fromContextDidSave: notification)
How do I let the NSFetchedResultsController in the primary target know something got saved from another target?
I'm setting up the persistentContainer like so:
lazy private var persistentContainer: NSPersistentContainer = {
let groupName = "group.com.contoso.CoolCalendar"
let url = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: groupName)!
.appendingPathComponent("CoolCalendar.sqlite")
let container = NSPersistentContainer(name: "CoolCalendar")
container.persistentStoreDescriptions = [
NSPersistentStoreDescription(url: url)
]
container.loadPersistentStores(completionHandler: { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()

Related

Core Data and CloudKit sync is inconsistent

I have an app that uses Core Data and CloudKit using the public database. The problem is that deletes never seem to sync and additions and changes don't show up until the app enters the background and then returns to the foreground, and even those results are inconsistent.
The example app is just the default app you get when specifying SwiftUI and Core Data. I modified the Schema in the CloudKit dashboard to add the two indexes recordName and modifiedAt.
The following is the Persistence.swift file, so to reproduce, create a new project, chose Core Data and Use CloudKit, Add Capability for CloudKit and Remote Notifications, then replace the Persistence.swift contents with the above.
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 {
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "TestCKSink")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
guard let description = container.persistentStoreDescriptions.first else
{
fatalError()
}
//description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.cloudKitContainerOptions?.databaseScope = .public
container.viewContext.transactionAuthor = "Me"
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}
I have tried this with and without the history tracking option turned on. As I said, this works some of the time. I also realize that I need to add code to force refresh the display, but even checking the CloudKit dashboard and querying the records, some do not don't show there at all or take a long time to arrive (18 minutes).
Any suggestions would be welcomed.
I experienced similar issues some time ago, try the following setting when initializing your managed object context:
managedObjectContext.automaticallyMergesChangesFromParent = true

Migrating Data to App Groups Disables iCloud Syncing

I am adding a Today Extension to my existing app. I have added the an App Group and used this post to successfully migrate my Core Data's data to the App Group's store. My app uses both a NSPersistentCloudKitContainer (when iCloud is toggled on) and a NSPersistentContainer (iCloud toggled off). While the data in both containers migrate successfully, I am no longer able to sync between my devices when using NSPersistentCloudKitContainer. In the console I get these two errors:
CoreData: error: CoreData+CloudKit: -[NSCloudKitMirroringDelegate _performSetupRequest:]_block_invoke(837): : Failed to set up CloudKit integration for store: (URL: file://path/name.sqlite)
Error Domain=NSCocoaErrorDomain Code=134060 "A Core Data error occurred." UserInfo={NSLocalizedFailureReason=The mirroring delegate could not initialize because it's store was removed from the coordinator.}
The path from the first error message is the path of the oldURL before switching to App Groups. So I believe I just need to tell iCloud to not try to integrate CloudKit at that store location and use the App Group's store location.
But I can not figure out how to do this. Can anyone help?
Core Data code:
class CoreDataManager {
static let sharedManager = CoreDataManager()
private init() {}
lazy var persistentContainer: NSPersistentContainer = {
var useCloudSync = UserDefaults.standard.bool(forKey: "useCloudSync")
//Get the correct container
let containerToUse: NSPersistentContainer?
if useCloudSync {
containerToUse = NSPersistentCloudKitContainer(name: "App")
} else {
containerToUse = NSPersistentContainer(name: "App")
}
guard let container = containerToUse else {
fatalError("Couldn't get a container")
}
//Set the storeDescription
let storeURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.App")!.appendingPathComponent("\(container.name).sqlite")
var defaultURL: URL?
if let storeDescription = container.persistentStoreDescriptions.first, let url = storeDescription.url {
defaultURL = FileManager.default.fileExists(atPath: url.path) ? url : nil
}
if defaultURL == nil {
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
}
let description = container.persistentStoreDescriptions.first else {
fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
}
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
if !useCloudSync {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
//migrate from old url to use app groups
if let url = defaultURL, url.absoluteString != storeURL.absoluteString {
let coordinator = container.persistentStoreCoordinator
if let oldStore = coordinator.persistentStore(for: url) {
do {
try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
} catch {
print("Hey Listen! Error migrating persistent store")
print(error.localizedDescription)
}
// delete old store
let fileCoordinator = NSFileCoordinator(filePresenter: nil)
fileCoordinator.coordinate(writingItemAt: url, options: .forDeleting, error: nil, byAccessor: { url in
do {
try FileManager.default.removeItem(at: url)
} catch {
print("Hey Listen! Error deleting old persistent store")
print(error.localizedDescription)
}
})
}
}
}
return container
}
}
If you are still facing the same problem, you should add the following line for the storeDescription
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.yourapp.identifier")
Source: https://developer.apple.com/videos/play/wwdc2019/202/
Following is my CoreDataStack:
import CoreData
class CoreDataStack {
// MARK: - Core Data stack
static var persistentContainer: NSPersistentCloudKitContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentCloudKitContainer(name: "data")
let storeURL = URL.storeURL(for: "group.com.myapp", databaseName: "data")
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.myapp")
container.persistentStoreDescriptions = [storeDescription]
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)")
}
})
return container
}()
// MARK: - Core Data Saving support
static func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.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)")
}
}
}
}
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")
}
}

Core Data: how to reset everything when setup with loadPersistentStores?

When using the "new" iOS 10 Core Data setup with loadPersistentStores, how can I reset/delete everything? I would like to avoid using the entity names but would love to use something like destroyPersistentStore.
I setup my stack the following:
persistentContainer = NSPersistentContainer(name: "CoreData", managedObjectModel: mom)
let storeDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let url = storeDirectory.appendingPathComponent("CoreData.sqlite")
let description = NSPersistentStoreDescription(url: url)
description.shouldInferMappingModelAutomatically = true
description.shouldMigrateStoreAutomatically = true
persistentContainer.persistentStoreDescriptions = [description]
persistentContainer.loadPersistentStores(completionHandler: { (_, error) in
guard let error = error as NSError? else { return }
fatalError("Unresolved error: \(error), \(error.userInfo)")
})
persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
persistentContainer.viewContext.undoManager = nil
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
I only found solutions with e.g. BatchDeleteRequests where I need to know all entity names, but I would like something more generic and reset everything - I need the most performant/safest way when the user logs out.
This suggestion performs the following steps
Get the store from the persistentStoreCoordinator
Save the URL of the store
Reset the managed object context
Destroy the store
Add a new store
The code assumes the standard implementation of the iOS 10+ NSPersistentContainer API with persistentStoreCoordinator and managedObjectContext properties in AppDelegate and a SQLite store.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let persistentStoreCoordinator = appDelegate.persistentContainer.persistentStoreCoordinator
let currentStore = persistentStoreCoordinator.persistentStores.last!
let currentStoreURL = currentStore.url!
appDelegate.managedObjectContext.reset()
do {
try persistentStoreCoordinator.destroyPersistentStore(at: currentStoreURL, ofType: NSSQLiteStoreType)
try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: currentStoreURL)
} catch {
print(error)
}
In iOS I'd recommend to add a new store immediately because normally the application won't terminate and therefore the persistent container won't be recreated.

Core Data for iOS 9 in Swift 3 not working

I am developing a project in XCode 8 with Core data Functionality. Xcode 8 does support Core Data functionality for iOS 10 and up but when I try to implement it for iOS 8 and 9, it crashes when getting managedContext with the message:
fatal error: unexpectedly found nil while unwrapping an Optional value
for the code:
let managedContext = appDelegate.managedObjectContext
App Delegate:
// MARK: - Core Data stack
#available(iOS 10.0, *)
lazy var persistentContainer: NSPersistentContainer = {
/*
The persistent container for the application. This implementation
creates and returns a container, having loaded the store for the
application to it. This property is optional since there are legitimate
error conditions that could cause the creation of the store to fail.
*/
let container = NSPersistentContainer(name: "Feel")
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)")
}
})
return container
}()
lazy var applicationDocumentsDirectory: URL = {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return urls[urls.count-1]
}()
lazy var managedObjectModel: NSManagedObjectModel = {
// The managed object model for the application. This property is not optional. It is a fatal error for the application not to be able to find and load its model.
let modelURL = Bundle.main.url(forResource: "coreDataTemplate", withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
// The persistent store coordinator for the application. This implementation creates and returns a coordinator, having added the store for the application to it. This property is optional since there are legitimate error conditions that could cause the creation of the store to fail.
// Create the coordinator and store
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let url = self.applicationDocumentsDirectory.appendingPathComponent("SingleViewCoreData.sqlite")
var failureReason = "There was an error creating or loading the application's saved data."
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
} catch {
// Report any error we got.
var dict = [String: AnyObject]()
dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject?
dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject?
dict[NSUnderlyingErrorKey] = error as NSError
let wrappedError = NSError(domain: "YOUR_ERROR_DOMAIN", code: 9999, userInfo: dict)
// Replace this with code to handle the error appropriately.
// abort() 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.
NSLog("Unresolved error \(wrappedError), \(wrappedError.userInfo)")
abort()
}
return coordinator
}()
lazy var managedObjectContext: NSManagedObjectContext = {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) This property is optional since there are legitimate error conditions that could cause the creation of the context to fail.
let coordinator = self.persistentStoreCoordinator
var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = coordinator
return managedObjectContext
}()
// MARK: - Core Data Saving support
func saveContext () {
if #available(iOS 10.0, *) {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.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)")
}
}
} else {
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// abort() 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
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
abort()
}
}
}
}
Fetching Code:
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
if #available(iOS 10.0, *) {
let managedContext =
appDelegate.persistentContainer.viewContext
let fetchRequest =
NSFetchRequest<NSManagedObject>(entityName: "Test")
do {
tests = try managedContext.fetch(fetchRequest)
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
} else {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Test")
do {
let results =
try managedContext.fetch(fetchRequest)
tests = results as! [NSManagedObject]
} catch let error as NSError {
print("Could not fetch \(error), \(error.userInfo)")
}
}
How can I implement Core Data for an iOS project with deployment version being iOS 8.2 in XCode 8 using Swift 3?
Are you using a different model name in the iOS 8/9 code? Could you try replacing "coreDataTemplate" with "Feel" in your AppDelegate's managedObjectModel property like this:
let modelURL = Bundle.main.url(forResource: "Feel", withExtension: "momd")!
I'm not sure why it's throwing this particular error, but eliminating the following line might fix it:
let appDelegate = UIApplication.shared.delegate as! AppDelegate
Note that you already have the appDelegate from the guard statement above, and all you're doing here is creating a new constant with the same name, within a lower context. So, you can easily eliminate this line. (Actually, I should think that this downcasting would fail and generate a different error.)
There are a couple other recommendations I would make.
First, most importantly, cut out all the code relating to the Persistent Store
Coordinator. It's redundant. Instead, always use the managedObjectContext property
from the app delegate. That will still work in iOS 10.
(By using both the persistent store coordinator and the pre-iOS 10
code, you are just creating double the work for yourself. And,
you're likely to get a lot more bugs, as you just did here.)
The other recommendation is just a matter of preferred practice. You
can create a managedObjectContext property in each of the view controllers.
And, in the app delegate, set all of them to the app delegate's
managedObjectContext. As a matter of preferred practice, the view
controllers generally should not be referring back to the app
delegate.

Understanding Core Data When Deleting Objects that Depend on Each Other

This question is asking for the best practice in the following scenario:
Attached are images showing my work orders and services core data entities. Note that the Delete Rule is currently No Action for Work Order. (Note changing to Nullify will not fix my issue, just causes same issue). Also take note that on Service I have constraints on id. This won't allow duplicates. As such I aded a merge policy below:
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
The merge policy will take the new data I send and overwrite what is in the database as the default. Without this my program will throw an error with how its written.
If I run my code with these settings, and I do a batch delete on workorders BUT NOT SERVICES (because I want to keep those) what happens is when I restart my program it crashes when I try to add **a reference to a Service with the same id.
My question is why would it crash and what is the best way to work around this? My current theory is that these entities might have another unique identifier and because I deleted the work order its reference was to a different contexted version of services... and when I create the new one using the same id as the old services it assumes the same internal id possibly. I am not sure if this is happening though or how to confirm that.
My code happens in viewDidLoad method of one of my controllers and looks like this.
override func viewDidLoad() {
super.viewDidLoad()
// Uncomment the following line to preserve selection between presentations
// self.clearsSelectionOnViewWillAppear = false
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem()
let context = gm_getContext()
//Create default fetch request to get all workorders
let fetchRequest: NSFetchRequest<Workorders> = Workorders.fetchRequest()
do{
//Run fetch request to get search results.
let searchResults = try context.fetch(fetchRequest)
//If no results were found and demo mode = true, lets create some default records.
if(searchResults.count<=0 && g_demoMode==true){
print("create default data")
//Uncomment the following lines if you want to prove that the Merge Policy
//Is working for Unique Constraints.
let serviceFetchRequest: NSFetchRequest<Service> = Service.fetchRequest()
let serviceSearchResults = try context.fetch(serviceFetchRequest)
print("Services Count = \(serviceSearchResults.count)")
//First we have to create a sample service
let entity = NSEntityDescription.entity(forEntityName: "Service", in: context)
let service = NSManagedObject(entity: entity!, insertInto: context)
service.setValue(1, forKey: "id")
service.setValue("Tire Repair Service Sample", forKey: "name")
service.setValue("<html>Test Service Field</html>",forKey:"templatedata")
//add reference to the global
g_services.append(service as! Service)
//Proof that service is indeed a Service object and stored in global
print("g_services[0].name = "+g_services[0].name!)
//Save the service object (overwriting an old one with same id if needed)
do {
try context.save()
print("Saved context with service")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
print("Could not save, unknown error")
}
//Now create 3 sample work orders all using the same service template.
let workorderEntity1 = NSEntityDescription.entity(forEntityName: "Workorders", in: context)
let workorder1 = NSManagedObject(entity: workorderEntity1!, insertInto: context)
print("created work order variable 1")
workorder1.setValue(1, forKey: "id")
workorder1.setValue("11402 Kensington Rd, Los Alamitos, CA, 90720", forKey: "address")
workorder1.setValue("33.797472", forKey: "lat")
workorder1.setValue("-118.084136", forKey: "lng")
workorder1.setValue(15,forKey: "client_id")
workorder1.setValue("Need to fix their tire fast", forKey: "desc")
workorder1.setValue("(562)810-4384", forKey: "phone")
workorder1.setValue(g_services[0], forKey: "service")
print("Created first work order")
let workorderEntity2 = NSEntityDescription.entity(forEntityName: "Workorders", in: context)
let workorder2 = NSManagedObject(entity: workorderEntity2!, insertInto: context)
workorder2.setValue(2, forKey: "id")
workorder2.setValue("17078 Greenleaf Street, Fountain Valley, CA, 92708", forKey: "address")
workorder2.setValue("33.714992", forKey: "lat")
workorder2.setValue("-117.958874", forKey: "lng")
workorder2.setValue(16,forKey: "client_id")
workorder2.setValue("This guy does not know what he wants", forKey: "desc")
workorder2.setValue("(562)777-3344", forKey: "phone")
workorder2.setValue(g_services[0], forKey: "service")
let workorderEntity3 = NSEntityDescription.entity(forEntityName: "Workorders", in: context)
let workorder3 = NSManagedObject(entity: workorderEntity3!, insertInto: context)
workorder3.setValue(3, forKey: "id")
workorder3.setValue("17045 South Pacific Avenue", forKey: "address")
workorder3.setValue("33.713565", forKey: "lat")
workorder3.setValue("-118.067535", forKey: "lng")
workorder3.setValue(17,forKey: "client_id")
workorder3.setValue("Tire damaged by the beach", forKey: "desc")
workorder3.setValue("(714)234-5678", forKey: "phone")
workorder3.setValue(g_services[0], forKey: "service")
//Don't need signature, pictures and videos because they just don't exist yet.
//add reference to the global
g_workOrders.append(workorder1 as! Workorders)
g_workOrders.append(workorder2 as! Workorders)
g_workOrders.append(workorder3 as! Workorders)
print("Preparing to save to context for work orders")
//Save the work order objects (overwriting any old ones with same id if needed)
do {
try context.save()
print("Saved context with workorders")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
print("Could not save, unknown error")
}
}else{
print("WorkOrders Count = \(searchResults.count)")
let workorderFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Workorders")
//let workorderFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Workorders")
let deleteWorkOrderRequest = NSBatchDeleteRequest(fetchRequest: workorderFetchRequest) //Deletes ALL workorders
//Perform Actual Deletion On Database Tables
do{
try context.persistentStoreCoordinator!.execute(deleteWorkOrderRequest, with: context)
}catch{
fatalError("Bad Things Happened \(error)")
}
print("deleted workorders")
}
} catch {
print("Error with request: \(error)")
}
print("service table view controller loaded")
}
My context and global variables to track the coreData values are defined globally in a globals.swift file like this.
var g_workOrders = [Workorders]()
var g_services = [Service]()
//Shortcut method to get the viewcontext easily from anywhere.
func gm_getContext () -> NSManagedObjectContext {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
//For unique constraints it will overwrite the data.
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context
}
Core Data Model References:
Other Notes & Things I've Tried:
I know it crashes at this line (workorder1.setValue(g_services[0], forKey: "service")), which is how I know its related to service, and changing the rule to cascade delete for workorders fixes the crash however it deletes the Services that were attached to it! ... which makes sense but not what I wanted.
I have recently found the answer to my question, and the problem is related to multiple things.
First my core data stack was set incorrectly. I've now changed it to this (courtesy my friendly developer friend who pointed this out).
import UIKit
import CoreData
class DataController: NSObject {
var managedObjectContext: NSManagedObjectContext
static var dataController: DataController!
override init() {
// This resource is the same name as your xcdatamodeld contained in your project.
guard let modelURL = Bundle.main.url(forResource: "WorkOrders", withExtension: "momd") else {
fatalError("Error loading model from bundle")
}
// The managed object model for the application. It is a fatal error for the application not to be able to find and load its model.
guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
let psc = NSPersistentStoreCoordinator(managedObjectModel: mom)
managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
managedObjectContext.persistentStoreCoordinator = psc
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let docURL = urls[urls.endIndex-1]
/* The directory the application uses to store the Core Data store file.
This code uses a file named "DataModel.sqlite" in the application's documents directory.
*/
let storeURL = docURL.appendingPathComponent("WorkOrders.sqlite")
do {
let options = [NSSQLitePragmasOption: ["journal_mode": "DELETE"]]
try psc.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
} catch {
fatalError("Error migrating store: \(error)")
}
}
class func sharedInstance() -> DataController {
if (dataController != nil) {
return dataController
}
dataController = DataController()
return dataController
}
}
Whenever I need to access coreData I should be doing it this way now...
let context = DataController.sharedInstance().managedObjectContext
Another thing to note is the concurrency setting in the Datacontroller is set to work on the main thread. This was also part of the problem since I was running my code in a thread.
Its set to the main thread on this line in DataController
managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
So everytime you are going to access or save data to coreData always wrap it in a call to the main thread like below...
DispatchQueue.main.async {
AppDelegate.appDelegate.saveContext()
}
Finally, the last problem I had was I was doing a batch delete with the following command below.
let workorderFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Workorders")
let deleteWorkOrderRequest = NSBatchDeleteRequest(fetchRequest: workorderFetchRequest) //Deletes ALL workorders
let context = DataController.sharedInstance().managedObjectContext
//Save the work order objects (overwriting any old ones with same id if needed)
do {
try context.execute(deleteWorkOrderRequest)
context.reset()
print(">>> cleared old data!")
} catch let error as NSError {
print("Could not save \(error), \(error.userInfo)")
} catch {
print("Could not save, unknown error")
}
The key here is understanding that batch commands currently work directly on the database and ignore the managed context, this means my managed context and database were getting out of sync after I ran this command. The easy fix is to always make sure after doing batch commands to run...
context.reset()
This will forcefully load back the data from the database into the managed context so everything is in sync. After I made these changes everything worked fine. Hope this helps someone.