David Schilling
David Schilling

Reputation: 2768

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?

Upvotes: 6

Views: 2420

Answers (2)

Paragate
Paragate

Reputation: 3

Think it depends on how to keep track of your RelationShips.

If One To Many Relation:

  • SchoolClass can have many SchoolClassSubject's
  • SchoolClassSubject can only belong to one SchoolClass

@Model
class SchoolClass {
    var name: String
    @Relationship(deleteRule: .cascade)
    var subjects = [SchoolClassSubject]() 
    init(name: String) {
        self.name = name
    }
}

@Model
class SchoolClassSubject {
    var subject: String

    @Relationship(inverse: \SchoolClass.subjects)
    var schoolClass: SchoolClass?  // ? meaning of optional 

    init(subject: String) {
        self.subject = subject
    }
}

Then in WindowGroup you have to have

struct MyApp: App {
    ...

    var body: some Scene {
        WindowGroup {
            ContentView(schoolClass: schoolClass)
                .modelContainer(for: SchoolClass.self)
        }
    }
}

In Views

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

    let schoolClass: SchoolClass

    var body: some View {
        Button("Save", action: save)
    }

    private func save() {
        let subject = SchoolClassSubject(subject: "String here..")

        // Either
        subject.schoolClass = schoolClass

        // Or
        schoolClass.subjects.append(subject)
    }
}

Upvotes: 0

Joakim Danielson
Joakim Danielson

Reputation: 51973

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)

Upvotes: 4

Related Questions