Kevin Olmats
Kevin Olmats

Reputation: 905

CoreData and SwiftUI: Context in environment is not connected to a persistent store coordinator

I am trying to teach myself Core Data by building a homework managing app. My code builds fine and the app runs okay until I try to add a new assignment to the list. I get this error Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1c25719e8) on the following line: ForEach(courses, id: \.self) { course in. The console also has this error: Context in environment is not connected to a persistent store coordinator: <NSManagedObjectContext: 0x2823cb3a0>.

I know very little about Core Data and am at a loss as to what the issue might be. I have set up "Assignment" and "Course" entities in the data model, where Course has a one-to-many relationship to Assignment. Each assignment will be categorized under a particular course.

This is the code for the view that adds a new assignment to the list:

    struct NewAssignmentView: View {

    @Environment(\.presentationMode) var presentationMode
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(entity: Course.entity(), sortDescriptors: []) var courses: FetchedResults<Course>

    @State var name = ""
    @State var hasDueDate = false
    @State var dueDate = Date()
    @State var course = Course()

    var body: some View {
        NavigationView {
            Form {
                TextField("Assignment Name", text: $name)
                Section {
                    Picker("Course", selection: $course) {
                        ForEach(courses, id: \.self) { course in
                            Text("\(course.name ?? "")").foregroundColor(course.color)
                        }
                    }
                }
                Section {
                    Toggle(isOn: $hasDueDate.animation()) {
                        Text("Due Date")
                    }
                    if hasDueDate {
                        DatePicker(selection: $dueDate, displayedComponents: .date, label: { Text("Set Date:") })
                    }
                }
            }
            .navigationBarTitle("New Assignment", displayMode: .inline)
            .navigationBarItems(leading: Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }, label: { Text("Cancel") }),
                                trailing: Button(action: {
                                    let newAssignment = Assignment(context: self.moc)
                                    newAssignment.name = self.name
                                    newAssignment.hasDueDate = self.hasDueDate
                                    newAssignment.dueDate = self.dueDate
                                    newAssignment.statusString = Status.incomplete.rawValue
                                    newAssignment.course = self.course
                                    self.presentationMode.wrappedValue.dismiss()
                                }, label: { Text("Add").bold() }))
        }
    }
}

EDIT: Here is the code in AppDelegate that sets up the persistent container:

lazy var persistentContainer: NSPersistentCloudKitContainer = {
    let container = NSPersistentCloudKitContainer(name: "test")
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
        if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
        }
    })
    return container
}()

And the code in SceneDelegate that sets up the environment:

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
    // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
    // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).

    // Get the managed object context from the shared persistent container.
    let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

    // Create the SwiftUI view and set the context as the value for the managedObjectContext environment keyPath.
    // Add `@Environment(\.managedObjectContext)` in the views that will need the context.
    let contentView = ContentView().environment(\.managedObjectContext, context)

    // Use a UIHostingController as window root view controller.
    if let windowScene = scene as? UIWindowScene {
        let window = UIWindow(windowScene: windowScene)
        window.rootViewController = UIHostingController(rootView: contentView)
        self.window = window
        window.makeKeyAndVisible()
    }
}

Upvotes: 37

Views: 15218

Answers (2)

mawus
mawus

Reputation: 1198

Environment values like your moc are automatically passed only to other views in the hierarchy. So if you show a sheet or anything that is not part of your view hierarchy you'll lose the environment and you will need to pass the moc to the new hierarchy, like you did for ContentView. Check this code snippet:

.sheet(isPresented: self.$showSheet) {
            SheetView()
                .environment(\.managedObjectContext, self.moc)
        }

Upvotes: 50

gotnull
gotnull

Reputation: 27224

You're not actually saving the context. You should be executing the following:

let newAssignment = Assignment(context: self.moc)
newAssignment.name = self.name
newAssignment.hasDueDate = self.hasDueDate
newAssignment.dueDate = self.dueDate
newAssignment.statusString = Status.incomplete.rawValue
newAssignment.course = self.course

do {
    try self.moc.save()
} catch {
    print(error)
}

Also your @FetchRequest(...) could look like this:

@FetchRequest(fetchRequest: CourseItem.getCourseItems()) var courses: FetchedResults<CourseItem>

You can modify your CourseItem class to handle the sortDescriptors like the following:

public class CourseItem: NSManagedObject, Identifiable {
    @NSManaged public var name: String?
    @NSManaged public var dueDate: Date?
    // ...etc
}

extension CourseItem {
    static func getCourseItems() -> NSFetchRequest<CourseItem> {
        let request: NSFetchRequest<CourseItem> = CourseItem.fetchRequest() as! NSFetchRequest<CourseItem>

        let sortDescriptor = NSSortDescriptor(key: "dueDate", ascending: true)

        request.sortDescriptors = [sortDescriptor]

        return request
    }
}

Then you would modify your ForEach(...) like the following and can also handle the deletion of items quite easily as well:

ForEach(self.courses) { course in
    // ...
}.onDelete { indexSet in
    let deleteItem = self.courses[indexSet.first!]
    self.moc.delete(deleteItem)

    do {
        try self.moc.save()
    } catch {
        print(error)
    }
}

One thing you want to ensure is that the "Class Name" is set to "CourseItem", which matches the CourseItem class we created earlier.

Simply click ENTITIES in your .xcdatamodeId file and set everything to the following (including Module to "Current Product Module" and Codegen to "Manual/None"):

enter image description here

Upvotes: 8

Related Questions