Why is the entityForName on Environment(\.managedObjectContext).wrappedValue always nil? I get this error +entityForName: nil is not a legal NSPersistentStoreCoordinator for searching for entity name 'Project'

With @Environment(\.managedObjectContext) var viewContext I don't get this error. But I need to initialize the view with my controller that needs NSManagedObjectContext to be passed.

Can someone help to understand why these two lines don't return the same object? Or is it the same?

@main
struct umbrellaApp: App {
    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}
struct ContentView: View {
    @Environment(\.managedObjectContext) var viewContext // Works
    @StateObject private var controller: ContentViewController
    
    init() {
        // Crashes
        let viewContextValue = Environment(\.managedObjectContext).wrappedValue

        let controller = ContentViewController(managedObjectContext: viewContextValue)
        self._controller = StateObject(wrappedValue: controller)
    }
    
    
    var body: some View {
        NavigationView {
            Text("Hello World")
        }
    }
}

The initializer of ContentViewController.

init(managedObjectContext: NSManagedObjectContext) {
    self.managedObjectContext = managedObjectContext
    self.projectsController = NSFetchedResultsController(fetchRequest: Project.projectsFetchRequest,
      managedObjectContext: managedObjectContext,
      sectionNameKeyPath: nil, cacheName: nil)
        
    super.init()

    projectsController.delegate = self
    do {
        try projectsController.performFetch()
        self.projects = projectsController.fetchedObjects ?? []
    } catch {
        print("failed to fetch projects!")
    }
}
1

There are 1 best solutions below

4
On BEST ANSWER

Short answer, Environment needs the @, it is a wrapper. What you are trying to do isn't a documented use of Environment

https://developer.apple.com/documentation/swiftui/environment

Long answer,

You haven't provided a Minimal Reproducible product but here is what I see

    let viewContextValue = Environment(\.managedObjectContext).wrappedValue
     

It doesn't work because as you know the @Environment isn't available at this point or you would just use viewContext.

    let controller = ContentViewController(managedObjectContext: viewContextValue)

I see what you are trying to do here but as stated above @Environment just isn't available during init

    self._controller = StateObject(wrappedValue: controller)

And this while it "works" on the surface it kind of defeats the virtues of StateObject

SwiftUI might create or recreate a view at any time, so it’s important that initializing a view with a given set of inputs always results in the same view. As a result, it’s unsafe to create an observed object inside a view. Instead, SwiftUI provides the StateObject attribute for this purpose. You can safely create a Book instance inside a view this way: @StateObject var book = Book() https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

In my experience custom inits in SwiftUI don't provide a reliable experience. I try to stay away from them as much as I can. If you have to do custom work upon init do it in a class as a ViewModel/ViewController that is also an ObservableObject a View shouldn't do any work.

If you want an alternative to what you want to do this see this SO question

All you need is

let persistenceController = PersistenceController.shared

Inside your ContentViewController and initialize your StateObject like this.

@StateObject private var controller: ContentViewController = ContentViewController()

Here is a sample where I used a FetchedResultsController it has sections

import SwiftUI
import CoreData
class TaskListViewModel: ObservableObject {
    let persistenceController = PersistenceController.previewAware()
    @Published var fetchedResultsController: NSFetchedResultsController<Task>?
    
    init() {
        setupController()
    }
    func setupController() {
        do{
            fetchedResultsController = try retrieveFetchedController(sortDescriptors: nil, predicate: nil, sectionNameKeyPath: #keyPath(Task.isComplete))
        }catch{
            print(error)
        }
    }
    func deleteObject(object: Task) {
        persistenceController.container.viewContext.delete(object)
        save()
    }
    func save() {
        do {
            if persistenceController.container.viewContext.hasChanges{
                try persistenceController.container.viewContext.save()
                objectWillChange.send()
            }else{
            }
        } catch {
            print(error)
        }
    }
}
//MARK: FetchedResultsController setup
extension TaskListViewModel{
    func retrieveFetchedController(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, sectionNameKeyPath: String) throws -> NSFetchedResultsController<Task> {
        
        return try initFetchedResultsController(sortDescriptors: sortDescriptors, predicate: predicate, sectionNameKeyPath: sectionNameKeyPath)
    }
    private func initFetchedResultsController(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, sectionNameKeyPath: String) throws -> NSFetchedResultsController<Task> {
        fetchedResultsController = getFetchedResultsController(sortDescriptors: sortDescriptors, predicate: predicate, sectionNameKeyPath: sectionNameKeyPath)
        //fetchedResultsController!.delegate = self
        do {
            try fetchedResultsController!.performFetch()
            return fetchedResultsController!
            
        } catch {
            print( error)
            throw error
        }
    }
    func getFetchedResultsController(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, sectionNameKeyPath: String) -> NSFetchedResultsController<Task> {
        
        return NSFetchedResultsController(fetchRequest: getEntityFetchRequest(sortDescriptors: sortDescriptors, predicate: predicate), managedObjectContext: persistenceController.container.viewContext, sectionNameKeyPath: sectionNameKeyPath, cacheName: nil)
    }
    private func getEntityFetchRequest(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?) -> NSFetchRequest<Task>
    {
        
        let fetchRequest: NSFetchRequest<Task> = Task.fetchRequest()
        fetchRequest.includesPendingChanges = false
        fetchRequest.fetchBatchSize = 20
        if sortDescriptors != nil{
            fetchRequest.sortDescriptors = sortDescriptors
        }else{
            fetchRequest.sortDescriptors = [NSSortDescriptor(key: #keyPath(Task.dateAdded), ascending: false)]
        }
        if predicate != nil{
            fetchRequest.predicate = predicate
        }
        return fetchRequest
    }
}
struct TaskListView: View {
    @StateObject var vm: TaskListViewModel = TaskListViewModel()
    @State var taskToEdit: Task?
    var body: some View {
        
        if vm.fetchedResultsController?.sections != nil{
            List{
                ForEach(0..<vm.fetchedResultsController!.sections!.count){idx in
                    let section = vm.fetchedResultsController!.sections![idx]
                    TaskListSectionView(objects: section.objects as? [Task] ?? [], taskToEdit: $taskToEdit, sectionName: section.name).environmentObject(vm)
                    
                }
            }.sheet(item: $taskToEdit, onDismiss: {
                vm.save()
            }){editingTask in
                TaskEditView(task: editingTask)
            }
            
        }else{
            Image(systemName: "empty")
        }
    }
}

struct TaskEditView: View {
    @ObservedObject var task: Task
    var body: some View {
        TextField("name", text: $task.name.bound)
    }
}
struct TaskListSectionView: View {
    @EnvironmentObject var vm: TaskListViewModel
    let objects: [Task]
    @State var deleteAlert: Alert = Alert(title: Text("test"))
    @State var presentAlert: Bool = false
    @Binding var taskToEdit: Task?
    @State var isExpanded: Bool = true
    var sectionName: String
    
    var body: some View {
        Section(header: Text(sectionName) ,                content: {
            ForEach(objects, id: \.self){obj in
            let task = obj as Task
            Button(action: {
                taskToEdit = task
            }, label: {
                Text(task.name ?? "no name")
            })
            .listRowBackground(Color(UIColor.systemBackground))
            
            
        }.onDelete(perform: deleteItems)
        })
        
    }
    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            deleteAlert = Alert(title: Text("Sure you want to delete?"), primaryButton: Alert.Button.destructive(Text("yes"), action: {
                let objs = offsets.map { objects[$0] }
                
                for obj in objs{
                    
                    vm.deleteObject(object: obj)
                }
                //Because the objects in the sections aren't being directly observed
                vm.objectWillChange.send()
                
            }), secondaryButton: Alert.Button.cancel())
            
            
            self.presentAlert = true
            
        }
    }
}

struct TaskListView_Previews: PreviewProvider {
    static var previews: some View {
            TaskListView()
        
    }
}

previewAware() is just a method that decides wether to pass the built-in preview or shared

static func previewAware() -> PersistenceController{
    if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" {
        return PersistenceController.preview
    }else{
        return PersistenceController.shared
    }
}