Swift - how to pass managed object context to different view controllers - swift

I put the core data stack in its own file as shown below. I read that using dependency injection is the best way to pass the managed object context. So in each of a handful of VCs, I declare the following property:
var managedObjectContext: NSManagedObjectContext?
Now, the tricky part is getting the moc from my stack to the different VCs. Which seems like a great place for a singleton, but assuming that's a bad idea, I guess I would use the code below in CoreDataStack:
let controller = self.window!.rootViewController as! ViewController
let context = self.persistentContainer.viewContext
controller.managedObjectContext = context
But that leaves me with a few questions:
1) Where in CoreDataStack should I include the code above? In the App Delegate it would go in didFinishLaunchingWithOptions, but that's not really an option now.
2) Writing the above code for every single vc that needs a context seems bad. I guess I could loop through all the VCs. I've seen the moc passed using didSet too, but that doesn't seem quite right either.
CoreData Stack
class CoreDataStack {
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Model")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}

Related

Creating more than one entity on SwiftUI

I am trying to get more than one entity for my coding project at school but I have an error saying invalid redeclaration of data controller.
class DataController: ObservableObject{
let container = NSPersistentContainer(name: "Blood Sugar")
init() {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
class DataController : ObservableObject{
let containers = NSPersistentContainer(name: "Carbohydrates")
init(){
containers.loadPersistentStores{ description, errors in
if let errors = errors{
print("Core data failed to load: \(errors.localizedDescription)")
}
}
}
}
Since you tagged this as SwiftUI, DataController should be a struct. We use value types like structs now to solve a lot of the bugs caused by using objects in UIKit and ObjC. You can see Apple's doc Choosing Between Structures and Classes for more info.
If you use an Xcode app template project and check "Use core data" you'll see a PersistenceController struct that will demonstrate how to do it correctly. I've included it below:
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: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "SearchTest")
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)")
}
})
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
You can create more entities in the model editor. There is usually only one NSPersistentContainer per app. The container can have multiple stores (usually sqlite databases). Then you can assign different entities to each store too. To create an instance of an entity you do that on a NSManagedObjectContext and you can choose which store to save it too, although most of the time people use one store which is the default.

Getting nil managedObjectContext in CoreData with Swift working from a Library

I'm developing a library for iOS using Swift 5, and I want this library to use CoreData independent of the application which consumes that library and this is what I've done so far:
Created the entities with their respective data types
Created the .xcdatamodeld file, which contains the entities
Created a CoreDataManager which looks like this:
// MARK: - CoreDataManager
final class CoreDataManager {
static let shared = CoreDataManager()
private static let defaultObject = NSManagedObject.init()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Audit")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
}
And the trouble is I'm trying to get the context by:
let coreDataManager = CoreDataManager.shared
let context = coreDataManager.persistentContainer.viewContext
and context is returning nil
please help
I solved it, and apparently the trouble was that the ios application where I wanted to use my library wasn't finding the .xcdatamodeld file which resulted in a useless NSPersistentContainer object, which also meant that let context = persistentContainer.viewContext was nil.
In order to avoid this kind of troubles in the future, I'll left a list of important considerations when working with CoreData and swift libraries.
Key things to consider
Make sure the app that is consuming your library knows exactly where
to look for it. Might want to take a look at this article for details.
If you are working with cocoapods for distributing your library, make sure to add the following to your .podspec:
s.resources = "path/to/model.xcdatamodeld"
This will produce a folder named "Resources" in your Pods target:
Make sure your model file name matches the NSPersistentContainer name.
(NOT SURE ABOUT THIS) I changed the class definition of my NSManagedObjects from
class Audit: NSManagedObject {}
to
public class Audit: NSManagedObject {}
And even when I'm not sure if that makes sense, It could work for you.
Finally I'll leave the code that worked for me
// MARK: - CoreDataManager
final class CoreDataManager {
static let shared = CoreDataManager()
private static let defaultObject = NSManagedObject.init()
lazy var persistentContainer: NSPersistentContainer? = {
let modelURL = Bundle(for: Audit.self).url(forResource: "Audit", withExtension: "momd")
guard let model = modelURL.flatMap(NSManagedObjectModel.init) else {
print("Fail to load the trigger model!")
return nil
}
let container = NSPersistentContainer(name: "Audit", managedObjectModel: model)
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
}
And to get the context outside
let coreDataManager = CoreDataManager.shared
guard
let context = coreDataManager.persistentContainer?.viewContext
else {
print("Nil context case")
return
}
Hope you guys find it helpful!!

Knowing when lightweight migration finished

Setup:
I'm trying to do a lightweight Core Data migration in my app. I added a new Core Data model version and added the new improvements (added 9 attributes to existing entities, 2 new entities, relationships between the new and existing entities).
Issue:
Switching from a build with the old database to the new one causes the app to have no data. But once I quit the app and come back, the data is all there (I'm assuming it was just being migrated).
Question:
Is there a way to know when the Core Data is starting a migration (to let the users know) and when the migration is finished (to refresh their view with the new data)? I've done so much searching this past couple of weeks and have not come with much.
Thank you in advance!
Code:
This is how I set up Core Data (specifically the setupContainer() code):
final class CoreDataManager {
static let sharedManager = CoreDataManager()
lazy var persistentContainer: NSPersistentContainer = {
setupContainer()
}()
private func setupContainer() -> NSPersistentContainer {
useCloudSync = UserDefaults.standard.bool(forKey: UserDefaults.Keys.useCloudSync)
let containerToUse: NSPersistentContainer?
if useCloudSync {
containerToUse = NSPersistentCloudKitContainer(name: "appName")
} else {
containerToUse = NSPersistentContainer(name: "appName")
}
//check if we have a container
guard let container = containerToUse else {
fatalError("Hey Listen! Could not get a container!!")
}
// Enable history tracking and remote notifications
guard let description = container.persistentStoreDescriptions.first else {
fatalError("Hey Listen! ###\(#function): Failed to retrieve a persistent store description.")
}
//be notified of change
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
//set tracking history if we're using local container
if !useCloudSync {
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
}
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Hey Listen! Couldn't load persistent store. Unresolved error \(error), \(error.userInfo)")
}
})
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = appTransactionAuthorName
container.viewContext.automaticallyMergesChangesFromParent = true
//Remote changes notification
NotificationCenter.default.addObserver(
self, selector: #selector(type(of: self).storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator)
return container
}//end of setup container
}
Presumably you're calling loadPersistentStores on your NSPersistentContainer. That method accepts a completion handler (closure). That's where you'll get the callback that the migration has finished (or has failed). And you can tell when the migration is about to begin by adding a call just before loadPersistentStores.
If you create your own NSPersistentStoreDescription instance and add it to the NSPersistentContainer, you can control whether the migration happens and whether it runs synchronously or asynchronously.
See Enabling core data lightweight migration in Swift 3 (particularly answer https://stackoverflow.com/a/53607127/719690) for an example of using the completion handler.
Does the completionHandler of loadPersistentStores of NSPersistentContainer run synchronously? is also useful to understanding the flow.
Working from your code sample, you might revise it to (and I've switch to shortened closure syntax):
container.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
// error path, but should be nicer than just crashing
fatalError("Hey Listen! Couldn't load persistent store. Unresolved error \(error), \(error.userInfo)")
} else {
// happy path
print("unicorns and rainbows, migration succeeded, load the tableview")
}
}
(typed in browser, beware typos)
A Swiftier version would use a guard let on the error checking, but I wanted to minimize changes I made to your sample.

Simple macOs app gets EXC_BAD_ACCESS for CoreData accessing one to many object

I have very simple app, that has a CoreData database and in it, it has two type of objects, one that is the main object, and the other. The main object, Object A, can have many Object B. But Object B, can be connected to only one Object A.
My problem is, after a while of the app running, it runs into EXC_BAD_ACCESS error.
To be precise:
Thread 85: EXC_BAD_ACCESS (code=1, address=0x77e341213c20)
I have done some debugging, and it looks like this only happens, when I open the SwiftUI part of the interface, and possibly make changes to the db. I have read in forums, that it's a Thread issue and access. I tried database setup mentioed there (I am copying here) but that still runs into the error.
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
return result
}()
var context: NSManagedObjectContext {
return container.viewContext
}
var container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MyApp")
// turn on persistent history tracking
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
container.newBackgroundContext()
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)")
}
})
}
}
My question is, how can I debug this? I understand the fact, this is trying to access the array of Objects B, that is already released, I just don't understand why it's released. Could it be because I opened the SwiftUi window and closed it? But why doesn't the connection just keep it?
Is there a way to prevent this error? I can see 3 threds running in the debugger, when the excecption is thrown, but I'm not aware of "creating new thread" and being new to Swift, not sure how to start a one, or stop one from being created.
Apart from passing the context down directly to the view, in two places I use a helper that looks like this:
public func getManagedContext() -> NSManagedObjectContext {
return (NSApplication.shared.delegate as! AppDelegate).coreDataStack.context
}
You can try fetching values in DipatchQueue.main block
it will avoid blocking the current thread.

Core data: Failed to load model

I am new to core data.
What I am trying to DO: I am trying to create a cocoatouch framework that has an app to add employee details and display them in a table view. So that i can add this framework to my main project to work independently.
Issues I face: The frame work builds without any error. I have added the core data stack from swift 3 to the framework. But when i run the main project, the moment the framework loads the log displays "Failed to load model named Simple framework", "fetch failed" and "employee must have a valid entity description". The code that I have used in the framework is as shown below :
public class CoreDataStack {
public static let sharedInstance = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "SimpleFramework")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error {
fatalError("Unresolved error \(error), \(error)")
}
})
return container
}()
public func saveContext() {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
}
#IBAction func addEmployee(_ sender: Any) {
//To save the data
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
let employee = Employee(context: context)
employee.employeeName = nameTextField.text
employee.employeeAge = Int16(ageTextField.text!)!
employee.hasVehicle = hasVehicle.isOn
CoreDataStack.sharedInstance.saveContext()
navigationController!.popViewController(animated: true)
}
#IBAction func addEmployee(_ sender: Any) {
//To save the data
let context = CoreDataStack.sharedInstance.persistentContainer.viewContext
let employee = Employee(context: context)
employee.employeeName = nameTextField.text
employee.employeeAge = Int16(ageTextField.text!)!
employee.hasVehicle = hasVehicle.isOn
CoreDataStack.sharedInstance.saveContext()
navigationController!.popViewController(animated: true)
}
I've had this issue, when I had wrong model name - it should be models name, not the projects (see the screen shot)
Explicitly pass the models file name to the Core Data stack for initialization and make sure, it is loaded from the right bundle at the time (test bundle, app bundle...) by using Bundle(for: type(of: self)):
//...
let momdName = "SimpleFramework" //pass this as a parameter
//...
guard let modelURL = Bundle(for: type(of: self)).url(forResource: momdName, withExtension:"momd") else {
fatalError("Error loading model from bundle")
}
guard let mom = NSManagedObjectModel(contentsOf: modelURL) else {
fatalError("Error initializing mom from: \(modelURL)")
}
persistentContainer = NSPersistentContainer(name: momdName, managedObjectModel: mom)
//...
Edit:
Also make sure, the SimpleFramework.xcdatamodeld is added to the used targets Target Membership:
The string you pass to the NSPersistentContainer initializer:
NSPersistentContainer(name: "CoreData")
needs to match the filename of the data model file in your Xcode project:
CoreData.xcdatamodeld
If you want to use CoreData in your dynamic framework you have to subclass NSPersistentContainer and use it instead of NSPersistentContainer.
class PersistentContainer: NSPersistentContainer { }
//...
lazy var container: PersistentContainer = {
let result = PersistentContainer(name: "Your xcdatamodeld file name here")
result.loadPersistentStores { (storeDescription, error) in
if let error = error {
print(error.localizedDescription)
}
}
return result
}()
In my case, for some reason the DataModel.xcdatamodeld became missing from my project workspace.
First I tried creating a new DataModle.xcdatamodeld and recreating the data model, but the same error occurred. Thats when I realized that the Original DataModel.xcdatamodeld was still in the root directory. I fixed this by simply right clicking my project in my project navigator, and selecting "Add files to "Project"...", then I added my old data model and deleted my new data model. Finally I hard cleaned, ran my project and it fixed the issue.
My problem was at my .podspec file. You should include the xcdatamodeld extension on the pod that you are creating.
s.resources = "myprojectfolder/**/*.{png,jpeg,jpg,storyboard,xib,xcassets,xcdatamodeld}"