Using Core Data with SwiftUI App Protocol

Is it possible to use CoreData with the newly announces SwiftUI App Protocol for 100% SwiftUI apps. If I need to create an app with persistant storage, is there a way to achieve this with the new protocol? I like the idea of having my app fully compatable across all systems.

Thanks.

34 Replies

Yes, you can setup everything you need directly in your App as following:

Code Block swift
@main
struct SampleApp: App {
    @Environment(\.scenePhase) private var scenePhase
    var body: some Scene {
        WindowGroup {
            MovieList()
                .environment(\.managedObjectContext, persistentContainer.viewContext)
        }
        .onChange(of: scenePhase) { phase in
            switch phase {
            case .active:
                print("active")
            case .inactive:
                print("inactive")
            case .background:
                print("background")
                saveContext()
            }
        }
    }
    var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "SampleApp")
        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)")
            }
        }
    }
}


Yes, you can setup everything you need directly in your App
@spellcasterdragonborn

Hah, Apple uses the underscore to format italic text, so when I type your name in here the underscores cause formatting chaos.

Yes, at this current moment in time, I agree with you - there is no need to use @StateObject.

So to be clear, at this current moment in time, I understand that ALL WE NEED to access Core Data features throughout our SwiftUI App is an NSManagedObjectContext.

To answer your question the long way round...

Apple does not let us edit our answers so when I wrote my original answer, this was all fairly new to me. If I could change the answer - as I have in my SwiftUI universal apps that use Core Data - I would change the following...

Code Block
    lazy var persistentContainer: NSPersistentContainer = {...}

Change to:

Code Block
private lazy var persistentContainer: NSPersistentContainer = {...}

My reason is that there are only three properties that I believe should be publicly available in my class implementation of PersistentStore...
  1. static let shared, ...and through this one line singleton...

  2. var context, to use throughout the app; and

  3. func save, for convenience.

If you check Apple's response to my forum question with the abstract title New toolbar appearance in macOS 11, you can see their advice "and then your SwiftUI App could hold on to an instance of that (ed. custom) class in a variable." In their response, they also suggest I should watch the SwiftUI Data Essentials WWDC session for inspiration. From my multiple viewings of this session, at the time I deducted that the hint was to use an @StateObject for my PersistentStore class, which is why I included it in my Core Data SwiftUI Xcode projects and in my original answer. It also made it easy for me to call save from the .onChange(of: scenePhase) view modifier.

In hindsight and following review of the template that Apple includes in more recent Xcode 12 betas, I was complicating their hint. (As per Apple's beta Core Data template) the instance of that persistent container custom class could be as simple as...
Code Block
    let persistenceController = PersistenceController.shared

or, per my previous answer...
Code Block
    let persistentStore = PersistentStore.shared

However, at this current moment in time, I would remove the @StateObject instance of my PersistentStore class. Not that I have done that yet in my SwiftUI projects (no reason, just not got around to it). It seems important for me to add subtext here - I am still developing an understanding of how @StateObject property wrapper operates in the App lifecycle, so in the future and with a better understanding, I might change this decision.

(PS: I just commented out @StateObject private var persistentStore = PersistentStore.shared in one project as a test and my iOS and macOS targets appear to build and run without issue. There seems to be a minor drop in memory usage, but this is speculative without taking steps to measure it properly.)

@Davidxo I was able to resolve the Cannot find 'Item' in scope error by doing the following:
  1. Quit Xcode

  2. Reopen Xcode

  3. Shift + ⌘ + K (clean)

  4. ⌘ + B to rebuild

In Beta 6 there is an option to use Core Data but the template it creates has an error.
Cannot find 'Item' in scope
I find this an odd bug because the data model contains an entity called Item.
Is there any way to resolve this?
Beta 5 now includes a template. But the template doesn't appear to have a saveContext for when the scene moves to the background. Am I missing something?
@andrewbuilder's answer is excellent. The only thing that needs to be changed is making "persistentContainer" a let instead of a lazy var. As as lazy var, the persistent container will not load in time for the first view. This caused a crash when my first view was trying to access my core data persistent container. However by making it a constant variable, this issue went away.
@andrewbuilder

Hey there. Is it necessary to make the persistentStore a @StateObject? Isn't it ok by just passing the context from your shared store into the environment, just like in the pre-generated code from Apple? I'm pretty new to SwiftUI so someone please explain if you can and have some time :) Thank you in advance!
My two cents...

My Core Data implementation for SwiftUI using the new App and Scene protocols and Window container!

Credit to:
  • mtsrodrigues for how to deal with change of Scene in his answer in this thread.

  • KrakenDev's article on how to write a one line singleton

My SwiftUI App...

Code Block
import SwiftUI
@main
struct YourApp: App {
    @Environment(\.scenePhase) private var scenePhase
    @StateObject private var persistentStore = PersistentStore.shared
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistentStore.context)
        }
        .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()
            @unknown default:
                fatalError("\(#function) REPORTS - fatal error in switch statement for .onChange modifier")
            }
        }
    }
    func savePersistentStore() {
        persistentStore.save()
    }
}

My "Core Data stack" - essentially Apple's template code embedded within a custom class that conforms to ObservableObject.

Code Block
import SwiftUI
import CoreData
class PersistentStore: ObservableObject {
    var context: NSManagedObjectContext { persistentContainer.viewContext }
    
// One line singleton
    static let shared = PersistentStore()
// Mark the class private so that it is only accessible through the singleton `shared` static property
    private init() {}
    private let persistentStoreName: String = "YourPersistentStoreName"
    // MARK: - Core Data stack
    lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: persistentStoreName)
// OR - Include the following line for use with CloudKit - NSPersistentCloudKitContainer
        // let container = NSPersistentCloudKitContainer(name: persistentStoreName)
        // Enable history tracking
        // (to facilitate previous NSPersistentCloudKitContainer's to load as NSPersistentContainer's)
        // (not required when only using NSPersistentCloudKitContainer)
        guard let persistentStoreDescriptions = container.persistentStoreDescriptions.first else {
            fatalError("\(#function): Failed to retrieve a persistent store description.")
        }
        persistentStoreDescriptions.setOption(true as NSNumber,
                                              forKey: NSPersistentHistoryTrackingKey)
        persistentStoreDescriptions.setOption(true as NSNumber,
                                              forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error {
                // Replace this implementation with code to handle the error appropriately.
                fatalError("Unresolved error \(error)")
            }
        })
// Include the following line for use with CloudKit - NSPersistentCloudKitContainer
        container.viewContext.automaticallyMergesChangesFromParent = true
// Include the following line for use with CloudKit and to set your merge policy, for example...
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy 
        return container
    }()
    // MARK: - Core Data Saving and "other future" support (such as undo)
    func save() {
        let context = persistentContainer.viewContext
        if !context.commitEditing() {
            NSLog("\(NSStringFromClass(type(of: self))) unable to commit editing before saving")
        }
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Customize this code block to include application-specific recovery steps.
                let nserror = error as NSError
                NSApplication.shared.presentError(nserror)
            }
        }
    }
}

To use the context within a Preview...

Code Block
struct YourSwiftUIStruct_Previews: PreviewProvider {
    static var previews: some View {
        let context = PersistentStore.shared.context
        return YourSwiftUIStruct()
            .environment(\.managedObjectContext, context)
    }
}


You can find a sample using Core Data with the SwiftUI Life Cycle here:
https://github.com/bmaciag/SwiftUI-App-with-Core-Data

@42Wowbaggers

Did you use lazy var persistentContainer? If so, then making it non-lazy should work.

While this will indeed silence the error, it'll also mean that a new persistent container is created each time persistentContainer is accessed. lazy is used to make sure the closure is only ever executed once.

IMO the best approach would be to move the initialisation of the container into a private init()—so the store can only be accessed through the shared singleton—and make persistentContainer immutable.

Code Block swift
class PersistentStore {
static let shared = PersistentStore()
let persistentContainer: NSPersistentContainer
private init() {
self.persistentContainer = // ...
}
}


@nicolaeturcan

There is an error at line: .environment(\.managedObjectContext, persistentContainer.viewContext) Error: "Cannot use mutating getter on immutable value: 'self' is immutable"

Did you use lazy var persistentContainer? If so, then making it non-lazy should work.


I think a few people have suggested something similar to what m2ike suggested:
Code Block
MainView().environment(\.managedObjectContext, PersistentStore.shared.persistentContainer.viewContext)


It certainly works to read the viewContext from the PersistentStore. However, speaking only for myself, I'm looking for a way to create a wrapper class around the NSPersistentContainer instance that can be passed around the entire view hierarchy, and not only pass around the viewContext. The reason for this is that I want custom methods for saving, creating new entity objects, fetching specific sets entities, etc. In order to do this, there seem to be at least 4 different approaches:
  1. Pass the context through the \.managedObjectContext environment key, and recreate all the save, creating, fetching functions wherever you read the context [Obviously this would be a silly approach, with a lot of duplicated code prone to errors]

  2. Create a helper class that contains all the save, creating, fetching functions, and pass that into the environment, while separately passing around the viewContext as described above. [This works, but it would be better to pass the context and the helper functions together].

  3. Pass the store instance into the environment as an environmentObject, but not using the \.managedObjectContext key [This seems like it should work, but the app kept crashing when it tried to read the fetched objects from the store - most likely I was doing something wrong.]

  4. Something like what AlbertUI proposed, but with some (currently unknown?) tweak to address the problem that the \.managedObjectContext key requires an NSManagedObjectContext object instead of a store singleton.

@DannyCamenisch, @ryanbdevilled - I think it should be PersistentStore.shared.persistentContainer.viewContext, which is an NSManagedObjectContext, instead of just PersistentStore.shared.

e.g.
Code Block
MainView().environment(\.managedObjectContext, PersistentStore.shared.persistentContainer.viewContext)

Thanks @mtsrodrigues and other contributors. I am developing an app that uses UIManagedDocument in conjunction with UIDocumentBrowserViewController and would like to migrate this to SwiftUI. It sees to me as though the new DocumentGroup stuff might be coaxed into using the same approach. Any ideas?