In the following program, I assumed that swift data would automagically handle all relationships. When a new exam is created, a roster is passed in and the intention is for all grades to be initialized to 0 for all students in the roster.
import SwiftUI
import SwiftData
extension String {
static func randomString(length: Int = 7) -> String {
let letters = "abcdefghijklmnopqrstuvwxyz"
return String((0..<length).map{ _ in letters.randomElement()! })
}
}
@Model
final class Grade {
var student : Student
var grade : Int
init(student: Student, grade: Int) {
self.student = student
self.grade = grade
}
}
@Model
final class Exam {
var name : String
var date : Date
var grades : [Grade]
init(name: String, date: Date, roster: [Student]) {
self.name = name
self.date = date
// initialize grades to 0 for every student in the given roster...
// this next line causes the following....
//
// *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
// reason: 'Illegal attempt to establish a relationship 'student' between objects in different contexts
// (source = <NSManagedObject: 0x600002217f70>
// (entity: Grade; id: 0x6000002b90a0 <x-coredata:///Grade/tB22D0958-A107-4FA1-B705-CA54127FB53232>; data: {
// grade = nil;
// student = nil;
// }) ,
// destination = <NSManagedObject: 0x6000021dd360>
// (entity: Student; id: 0xa40ac2d6f4136136 <x-coredata://5F5F57E9-7355-46B0-9143-D08161C9FA59/Student/p3>; data: {
// first = cmzngpm;
// last = odzlkmv;
// }))'
self.grades = roster.map( {Grade(student: $0, grade: 0)} )
}
}
@Model
final class Student {
var first : String
var last : String
init(first: String, last: String) {
self.first = first
self.last = last
}
}
@Model
final class Course {
var name : String
var roster : [Student]
var exams : [Exam]
init(name: String, roster: [Student]) {
self.name = name
self.roster = roster
self.exams = []
}
}
@main
struct StudentsApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([Course.self, Student.self, Exam.self, Grade.self])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
CourseListView()
}
.modelContainer(sharedModelContainer)
}
}
struct CourseListView : View {
@Environment(\.modelContext) private var modelContext
@Query private var courses: [Course]
var body: some View {
NavigationStack {
List {
ForEach(courses) { course in
NavigationLink {
CourseView(course: course)
} label: {
Text(course.name)
}
}
}
.navigationTitle("Courses")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {addCourse()}, label: {
Image(systemName: "plus")
})
}
}
}
}
private func addCourse() {
modelContext.insert(Course(name: String.randomString(), roster: []))
}
}
struct CourseView: View {
@Environment(\.modelContext) private var modelContext
@Bindable var course : Course
var body: some View {
NavigationStack {
List {
Section {
ForEach(course.exams) { exam in
NavigationLink {
ExamView(course: course, exam: exam)
} label: {
Text(exam.name)
}
}
} header: {
HStack {
Text("Exams")
Spacer()
Button(action: {addExam()}, label: {
Image(systemName: "plus")
})
}
}
Section {
ForEach(course.roster) { student in
NavigationLink {
Text("\(student.first) \(student.last)")
} label: {
Text("\(student.first) \(student.last)")
}
}
} header: {
HStack {
Text("Students")
Spacer()
Button(action: {addStudent()}, label: {
Image(systemName: "plus")
})
}
}
}
.navigationTitle(course.name)
}
}
private func addStudent() {
let student = Student(first: String.randomString(), last: String.randomString())
course.roster.append(student)
}
private func addExam() {
let exam = Exam(name: String.randomString(), date: Date(), roster: course.roster)
course.exams.append(exam)
}
}
struct ExamView : View {
@Bindable var course : Course
@Bindable var exam : Exam
init(course: Course, exam: Exam) {
self.course = course
self.exam = exam
}
var body : some View {
Text(exam.name)
ForEach(exam.grades) { grade in
NavigationLink {
Text("\(grade.student.first) \(grade.student.last)")
} label: {
Text("Exam: \(grade.student.first) \(grade.student.last)")
}
}
}
}
#Preview {
CourseListView()
.modelContainer(for: Course.self, inMemory: true)
}
Run the above code and
- add a course
- select the course
- add students
- add an exam
This results in app termination with the following error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: 'Illegal attempt to establish a relationship 'student' between objects in different contexts
(source = <NSManagedObject: 0x600002217f70>
(entity: Grade; id: 0x6000002b90a0 <x-coredata:///Grade/tB22D0958-A107-4FA1-B705-CA54127FB53232>; data: {
grade = nil;
student = nil;
}) ,
destination = <NSManagedObject: 0x6000021dd360>
(entity: Student; id: 0xa40ac2d6f4136136 <x-coredata://5F5F57E9-7355-46B0-9143-D08161C9FA59/Student/p3>; data: {
first = cmzngpm;
last = odzlkmv;
}))'
What is wrong with how the data is modeled and being used?
To fix this, I first added the inverse relationship between Student and Grades by updating Student as follows:
I updated Grade as follows, making Student optional
I then updated the Exam class initializer as follows
and all is fixed. Here is the fully updated program: