swiftuiswiftdataswift-data-relationship

SwiftData relationship updated only after app restart


I have 2 simple models with relation 1 to many. CarMake can have multiple CarModels but one CarModel can only belong to one CarMake. Pretty straightforward.

@Model
final class CarModel {
    var name: String
    var make: CarMake
    
    init(name: String, make: CarMake) {
        self.name = name
        self.make = make
    }
}

@Model
final class CarMake {
    var name: String
    @Relationship(inverse: \CarModel.make) var models: [CarModel] = []
    
    init(name: String) {
        self.name = name
    }
}

I created a view that will list all CarMakes and another that will list CarModels from the relationship property of CarMake and will also allow me to add a new CarModel to CarMake

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [CarMake]
    
    @State var count: Int = 1

    var body: some View {
        NavigationStack {
            List {
                ForEach(items) { item in
                    NavigationLink(item.name) {
                        CarMakeView(make: item)
                    }
                }
            }
            .toolbar {
                Button {
                    addItem(name: "Car Make \(count)")
                    count += 1
                } label: {
                    Image(systemName: "plus")
                }
            }
        }
    }
    
    private func addItem(name: String) {
        let newItem = CarMake(name: name)
        modelContext.insert(newItem)
    }
}

struct CarMakeView: View {
    @Environment(\.modelContext) private var modelContext
    
    @State var count: Int = 1
    var make: CarMake
    
    var body: some View {
        List {
            ForEach(make.models) { item in
                Text(item.name)
            }
        }
        .toolbar {
            Button {
                addItem(name: "Car Model \(count)")
                count += 1
            } label: {
                Image(systemName: "plus")
            }
        }
    }
    
    private func addItem(name: String) {
        let newItem = CarModel(name: name, make: make)
        modelContext.insert(newItem)
    }
}

Adding new CarModel object to the modelContext, as presented in private func addItem(name: String) will not cause the list of make.models(the relationship) to update. The data is added to the database correctly because if I restart the app the list will show all the added CarModels from previous app run. Changing screens does not matter, the relationship is not updated until the app is restarted. Can somebody help me to pin point what I'm doing wrong with this code?

I tried to simply add a new object with a relationship to another object and then list all the objects in 1 to many relationship. I expected data to be available as soon as I updated the database.


Solution

  • This is some kind of bug in SwiftData since you are not doing anything wrong.
    The issue is that your CarMakeView has a property make of type CarMake but you are not updating that property directly but via the relationship and this means that the view doesn't see anything dependent being updated and has no reason to update itself.

    This is the bug then because make is actually updated when a new car model is added but somehow this change to the relationship property isn't observed properly.

    There are some workarounds for this:

    The one I use if the relationship properties are optional is to append the new CarModel object to the make property instead because then we update the make property directly

    private func addItem(name: String) {
        let newItem = CarModel(name: name)
        make.models.append(newItem)
    }
    

    For your case we need to make the relationship property optional in CarModel though for this to work

    @Model
    final class CarModel {
        var name: String
        var make: CarMake?
    
        init(name: String, make: CarMake? = nil) {
            self.name = name
            self.make = make
        }
    }
    

    Another solution is make the view update for some other reason, in your example I simply made use of an existing property

    @State var count: Int = 1
    

    and added it to the body in a Text which will cause the view to redraw

    var body: some View {
        VStack {
            Text("\(count)")
            List {
                ForEach(make.models) { item in
                //...