Search code examples
swiftswiftuiswiftdata

SwiftData Modeling and saving non Nullable Relations


I have the following code:

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext

    var schoolClass: SchoolClass

    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
            Button("add", action: {
                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
    }
}

schoolClass is already saved in swiftData and passed as a property to 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 x-coredata:///SchoolClassSubject/t186670BB-357C-400C-8D6F-178BEB8F4B473; data: { schoolClass = nil; }) , destination = <NSManagedObject: 0x60000214cd70> (entity: SchoolClass; id: 0x600000276740 x-coredata:///SchoolClass/t186670BB-357C-400C-8D6F-178BEB8F4B472; data: { name = test; schoolClassSubjects = ( ); }))' *** First throw call

If I change my Model to this:

@Model
class SchoolClassSubject {
    var schoolClass: SchoolClass? = nil
    init() {
    }
}

And my saving code to this:

let schoolClassSubject = SchoolClassSubject()
modelContext.insert(schoolClassSubject)
schoolClassSubject.schoolClass = schoolClass
schoolClass.schoolClassSubjects.append(schoolClassSubject)

But I don't want to make schoolClass in schoolClassSubject optional as it is not in reality. How can I make it not optional and still save it without the given error?


Solution

  • This answer was written using Xcode 15.0 beta 2, in future versions I assume this answer will become irrelevant and we can handle this more easily

    Once again, you must use the @Relationship annotation for the relationships to work properly and once you have that SwiftData will handle them for you.

    And because of that you should not include any relationship properties in any init methods for your model.

    So change the init in SchoolClassSubject to

    init() {}
    

    (I don't know if it is a bug that we need an empty init here or if this is just an odd case because there are no other properties)

    And then change the code in the button action to

    let schoolClassSubject = SchoolClassSubject()
    modelContext.insert(schoolClassSubject)
    schoolClassSubject.schoolClass = schoolClass
    

    To clarify the above code:

    • First create an instance of your model object
    • Then insert the object into the model context
    • Lastly assign any relationship properties to your object
    • (save)