I'm creating a new iOS App and wanted to start with SwiftUI instead of Storyboard as backwards compatibility is no problem in this case. Watching WWDC 2020 Videos I noticed the new Life Cycle Option: SwiftUI App. This seemed very interesting as it does not use Storyboards at all anymore, which seems cleaner to me.
Anyway how should I persist my Data since CoreData is not available for this option. I've read that people just manually added CoreData but this also seems odd to me since Apple obviously does not want this currently.
Update
This looks to be fixed now in Xcode 12.0
Original answer
It looks like currently there is no automated way. You need to create your own CoreData container. This can be done in the main app.
The example may look like this:
import CoreData
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistentContainer.viewContext)
}
}
var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "TestApp")
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)")
}
}
}
}
Note that you have to manually add the data model to your project.
You can take a look at this link for a better explanation:
Using Core Data with SwiftUI 2.0 and Xcode 12
Alternatively you can create a singleton for your Core Data Stack as proposed here:
Accessing Core Data Stack in MVVM application
Related
I've been experimenting with some changes in my project and have been adding and deleting Core Data instances of entities.
Eventually, due to changes in my code, the data already stored in persistent storage became corrupt, it fails to load. Whenever Core Data tries to load my data, the app crashes.
I know what the problem is - I need to delete some instances of core data entities from persistent storage. I can't, however, do it in the simulator by just swipe-deleting, because the app crashes when core data tries to load.
How do I delete everything in persistent storage through code, without loading the storage?
I know there is a function to destroy persistent store, but I honestly can't figure out how and where I am supposed to use it.
I presume I should change the code in my DataController.
Here is how I load NSPersistentContainer (DataController class)
import Foundation
import CoreData
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "iLegal")
init () {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
return
}
}
}
}
I then create a #StateObject of NSPersistentContainer and send it to the environment
import SwiftUI
#main
struct iLegalApp: App {
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
Please help!
#lorem-ipsum Thank you! I figured it out!
In my DataController class, I basically rewrote the init() function to:
init () {
do {
try container.persistentStoreCoordinator.destroyPersistentStore(at: container.persistentStoreDescriptions.first!.url!, type: .sqlite, options: nil)
print("Success")
} catch {
print(error.localizedDescription)
print("Fail")
}
I ran the project, the persistentStore was empty.
I then reverted to:
init () {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
return
}
}
}
}
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.
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.
I have the strangest problem. I’ve developed a SwiftUI app for iPhone, iPad and MacCatalyst using Core Data and CloudKit private database to sync the user’s data across all his/her devices.
The problem is that when I make an update on an iOS device (iPhone or iPad), the update syncs across all iOS devices, but not to the Macs. Similarly, updates I make on the Macs, sync across the Macs, but not to the iOS devices.
If I delete the app on the Mac and its associated sqllite database in the app’s associated ~/Library/Container/<myapp_container>/Data subfolder and reinstall the app, only the «Mac data» gets refilled from CloudKit. Likewise if I delete the app on an iOS device, only the «iOS data» arrives from CloudKit. In other words, it behaves as if the MacCatalyst data and the iOS data are stored separately in CloudKit.
Now, if I compile the app onto the Mac (rather than installing an Archive app), the sync from the iOS devices go through. But only once. Subsequent updates are not synced until I compile the app onto the Mac again. Likewise, updates made on the Mac only get synced (once) to the iOS devices when I compile the app on the Mac. Recompiling to the iOS devices does not make any difference.
The PersistentCloudkitContainer class:
public class PersistentCloudKitContainer {
// MARK: - Define Constants / Variables
public static var context: NSManagedObjectContext {
return persistentContainer.viewContext
}
// MARK: - Initializer
private init() {}
// MARK: - Core Data stack
public static var persistentContainer: NSPersistentContainer = {
let container = NSPersistentCloudKitContainer(name: "MyModel")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
guard let description = container.persistentStoreDescriptions.first else {
fatalError("### PersistentCloudKitContainer->\(#function): Failed to retrieve persistant store description")
}
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.cloudKitContainerOptions?.databaseScope = .private
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
// MARK: - Core Data Saving support
public static 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)")
}
}
}
The persistent container is called by the SwiftUI App struct:
struct MyApp: App {
let context = PersistentCloudKitContainer.persistentContainer.viewContext
var body: some Scene {
WindowGroup {
ContentView().environment(\.managedObjectContext, context)
}
}
}
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)")
}
}
}