observableswiftdataobservationobservation-framework

Observation and SwiftData Different Property Dependencies


Expected behaviour: The view is only updated when I edit the property text1 showed on screen

I'm getting the expected behavior whith Observation but not with SwiftData. Why? I thought SwiftData models are Observable

@Model
final class Item {
    var text1: String
    var text2: String
    
    init(text1: String, text2: String) {
        self.text1 = text1
        self.text2 = text2
    }
}

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]
    var body: some View {
        let _ = Self._printChanges()
        List{
            Text(items.first?.text1 ?? "")
            Button("Edit"){items[0].text2 = "Edited text2"}
        }
        .onAppear{
            modelContext.insert(Item(text1: "Initial text1",text2: "Initial text2"))
        }
    }
}
@Observable
final class Item {
    var text1: String
    var text2: String
    
    init(text1: String, text2: String) {
        self.text1 = text1
        self.text2 = text2
    }
}

struct ContentView: View {
    @State var item = Item(text1: "Initial text1",text2: "Initial text2")
    var body: some View {
        let _ = Self._printChanges()
        List{
            Text(item.text1)
            Button("Edit"){item.text2 = "Edited text2"}
        }
    }
}

Solution

  • This is not about the SwiftData model itself and its properties. What causes the refresh of the view is instead the @Query

    If we look in the logs we can first see that SwiftData posts a notification after the update has been done (button pressed).

    CoreData: debug: Remote Change Notification - posting for store ...

    This triggers a SwiftUI update and the print statement in your code prints

    ContentView: @dependencies changed.

    Which is directly followed by the query refreshing itself which can again be seen in the logs

    CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZTEXT1, t0.ZTEXT2 FROM ZITEM t0 ORDER BY t0.Z_PK

    So it is the query that reacts on the notification (dependency) and it does that because it only knows that something has changed but not exactly what so it needs to refresh itself and fetch all data again.

    This is a general disadvantage with @Query (and also @FetchRequest for Core Data) that they are quite "trigger happy" so to speak and often performs a refresh

    A possible workaround for this is to create a sub-view for the list content

    struct ItemRow: View {
        @Bindable var item: Item
        var body: some View {
            let _ = ItemRow._printChanges()
            VStack {
                Text(item.text1)
                Button("Edit"){
                    item.text2 = "\(Date().formatted(date: .omitted, time: .complete))"
                }
            }
        }
    }
    

    With this I still get a print from the ContentView when the button is pressed

    ContentView: @dependencies changed.

    but nothing is printed for ItemRow