iosarraysswiftenvironment-variables

Getting error when initializing core data context


I want to simply store a string array in core data and read it when I try and init the context I get the error "Variable 'self.context' used before being initialized". How can I fix this?

I think this is because the appDelegate hasn't be init, but it's an environment object that gets init in my main class initializer.

import Foundation
import SwiftUI
import UIKit
import CoreData

struct MyView {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var context: NSManagedObjectContext
    
    init() {
        context = appDelegate.persistentContainer.viewContext
    }
    
    func createStock(stockArray: [String]) {
        let newStock = Stock(context: context)
        newStock.stockArr = stockArray as NSObject

        do {
            try context.save()
        } catch {
            print("Error saving Stock: \(error.localizedDescription)")
        }
    }

    func fetchStocks() -> [Stock] {
        let fetchRequest: NSFetchRequest<Stock> = Stock.fetchRequest()

        do {
            let stocks = try context.fetch(fetchRequest)
            return stocks
        } catch {
            print("Error fetching Stocks: \(error.localizedDescription)")
            return []
        }
    }
}

Solution

  • As commented, the error "Variable 'self.context' used before being initialized" occurs because self.context is being assigned a value within the initializer before self.appDelegate has been initialized, hence the context cannot be properly set at this point.

    jnpdx's suggestion to change context to a NSManagedObjectContext computed property is a valid one, since it makes sure that context is derived from appDelegate whenever it is accessed, rather than trying to set it during initialization (which does lead to the error mentioned).

    struct MyView {
        @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
        var context: NSManagedObjectContext {
            return self.appDelegate.persistentContainer.viewContext
        }
    
        // rest of your code
    }
    
    +-------------------------+
    |       MyView            |
    | +---------------------+ |
    | | AppDelegate Adaptor | |
    | +---------------------+ |
    | +---------------------+ |
    | | Computed Property   | |
    | |   context           | |
    | +---------------------+ |
    +-------------------------+
    

    On the other hand, son suggests that the error is because appDelegate was not initialized at this time, which aligns with the original issue.
    See also "How to configure Core Data to work with SwiftUI" from Paul Hudson for illustration, using NSPersistentContainer.

    struct MyView {
        var store: 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
        }()
    
        var context: NSManagedObjectContext {
            return self.store.viewContext
        }
    
        // rest of your code
    }
    
    +------------------------------+
    |       MyView                 |
    | +--------------------------+ |
    | | NSPersistentContainer    | |
    | +--------------------------+ |
    | +--------------------------+ |
    | | NSManagedObjectContext   | |
    | | (from store.viewContext) | |
    | +--------------------------+ |
    +------------------------------+
    

    Both suggestions essentially serve to delay the access to appDelegate.persistentContainer.viewContext until after appDelegate has been initialized.

    jnpdx's suggestion is simpler and more direct, while son's suggestion provides a way to encapsulate the NSPersistentContainer within MyView itself, avoiding the dependency on appDelegate.


    The last approach, suggested by vadian is to separate the Core Data stack into its own class and passing the context into the environment.

    • It keeps the Core Data setup and management separate from your SwiftUI views. That separation makes the code more organized, easier to understand, and easier to maintain.
    • By encapsulating the Core Data stack in its own class, you can reuse it across different parts of your app without duplicating code.
    • SwiftUI provides the environment as a way to share data across views. By placing the NSManagedObjectContext in the environment, any view can access the context and perform Core Data operations.

    First, create a separate class for the Core Data stack:

    class CoreDataStack {
        static let shared = CoreDataStack()
        
        let 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
        }()
        
        var context: NSManagedObjectContext {
            return persistentContainer.viewContext
        }
    }
    

    Next, in your main SwiftUI file, you can inject the NSManagedObjectContext into the environment:

    @main
    struct MyApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .environment(\.managedObjectContext, CoreDataStack.shared.context)
            }
        }
    }
    

    Now, ContentView and all its subviews can access the NSManagedObjectContext through the environment, and perform Core Data operations as needed.

    +----------------------------+
    |    CoreDataStack           |
    | +-----------------------+  |
    | | NSPersistentContainer |  |
    | +-----------------------+  |
    | +------------------------+ |
    | | NSManagedObjectContext | |
    | +------------------------+ |
    +----------------------------+
             |
             | (shared instance)
             v
    +----------------------------+
    |        MyApp               |
    | +------------------------+ |
    | | Environment            | |
    | | (managedObjectContext) | |
    | +------------------------+ |
    +----------------------------+
             |
             | (environment injection)
             v
    +---------------------+
    |     ContentView     |
    |                     |
    +---------------------+
    

    When I use the first approach as a computed property, I get the error:

    Publishing changes from background threads is not allowed; 
    make sure to publish values from the main thread 
    (via operators like receive(on:)) on model updates.
    

    on the appDelegate.
    I think I have to use DispatchQueue but not sure how to implement it. Any tips?

    It means a SwiftUI view is attempting to update its UI from a background thread. In SwiftUI, all UI updates must be done on the main thread. That is a common issue when working with Core Data and SwiftUI, especially when Core Data operations are performed on a background thread.

    To make sure Core Data operations and subsequent UI updates are performed on the main thread, you may need to use DispatchQueue.main.async.

    struct MyView {
        // rest of your code
    
        func createStock(stockArray: [String]) {
            DispatchQueue.main.async {
                let newStock = Stock(context: self.context)
                newStock.stockArr = stockArray as NSObject
    
                do {
                    try self.context.save()
                } catch {
                    print("Error saving Stock: \(error.localizedDescription)")
                }
            }
        }
    
        func fetchStocks() -> [Stock] {
            var stocks: [Stock] = []
            DispatchQueue.main.sync {
                let fetchRequest: NSFetchRequest<Stock> = Stock.fetchRequest()
    
                do {
                    stocks = try self.context.fetch(fetchRequest)
                } catch {
                    print("Error fetching Stocks: \(error.localizedDescription)")
                }
            }
            return stocks
        }
    }
    

    In createStock, DispatchQueue.main.async is used to make sure the Core Data operations are performed on the main thread.
    Same for fetchStocks: DispatchQueue.main.sync is used to make sure fetching from Core Data occurs on the main thread.

    The DispatchQueue.main.async method schedules a block of code to be executed on the main thread at a later time, which can be useful for operations that update the UI. On the other hand, DispatchQueue.main.sync schedules a block to be executed on the main thread and waits for it to complete before continuing, which can be useful for operations that need to return a value.