Search code examples
swiftswiftuiswiftdataios17

SwiftData error NSInvalidArgumentException when assigning relationship


I have the following code:

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
            Button("add", action: {
                let schoolClass = SchoolClass(name: "test")
                modelContext.insert(schoolClass)
                
                let schoolClassSubject = SchoolClassSubject(schoolClass: schoolClass)
                schoolClass.schoolClassSubjects.append(schoolClassSubject)
            })
            
        }
        .padding()
    }
}

@Model
class SchoolClass {
    var name: String
    var schoolClassSubjects: [SchoolClassSubject] = []
    
    init(name: String) {
        self.name = name
    }
}

@Model
class SchoolClassSubject {
    var schoolClass: SchoolClass
    
    init(schoolClass: SchoolClass) {
        print("test")
        self.schoolClass = schoolClass
    }
}


#Preview {
    ContentView()
}

The line let schoolClassSubject = SchoolClassSubject(schoolClass: schoolClass) breaks with the following exception:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Illegal attempt to establish a relationship 'schoolClass' between objects in different contexts (source = <NSManagedObject: 0x60000214cc30> (entity: SchoolClassSubject; id: 0x60000024ebe0 ; data: {
    schoolClass = nil;
}) , destination = <NSManagedObject: 0x60000214cd70> (entity: SchoolClass; id: 0x600000276740 ; data: {
    name = test;
    schoolClassSubjects =     (
    );
}))'
*** First throw call 

Does anyone understand why? The exception mentions different contexts but the schoolClassSubject is completely new.

Any help is appreciated.


Solution

  • This is actually two problems that needs to be resolved.

    Firstly the property schoolClassSubjects should be a relationship so change

    var schoolClassSubjects: [SchoolClassSubject] = []
    

    to

    @Relationship(.nullify, inverse: schoolClass) var schoolClassSubjects: [SchoolClassSubject]
    

    Above I used .nullify as the delete rule but you need to pick one rule that fits your case.

    and secondly things needs to be done in the right order.

    Because you insert one object into the ModelContext first and then you create the other one and a relationship between them you have a situation where one is in a model context and the other one isn't, this is somewhat incorrectly reported as being in "different contexts".

    One way to fix this is to create both objects and the relationships and then insert them into the context and everything will be persisted as expected. I tried some variations on doing this and there seems to be more than one way to make this work but here is what I settled for:

    let schoolClass = SchoolClass(name: "test")
    let schoolClassSubject = SchoolClassSubject(schoolClass: schoolClass)
    modelContext.insert(schoolClass)
    modelContext.insert(schoolClassSubject)
    

    Note that we only have to set one end of the relationship now, SwiftData handles the inverse for us.