Any SwiftData change updates every SwiftUI view

Perhaps I just have the wrong expectations, but I discovered some odd behavior from SwiftData that sure seems like a bug to me...

If you make any change to any SwiftData model object — even just setting a property to its current value — every SwiftUI view that uses SwiftData is rebuilt. Every query and every entity reference, even if the property was set on a model class that is completely unrelated to the view.

SwiftUI does such a good job of optimizing UI updates that it's hard to notice the issue. I only noticed it because the updates were triggering my debug print statements.

To double-check this, I went back to Apple's new iOS app template — the one that is just a list of dated items — and added a little code to touch an unrelated record in the background:

@Model
class UnrelatedItem {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

@main
struct jumpyApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Item.self,
            UnrelatedItem.self
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()
    
    init() {
        let context = sharedModelContainer.mainContext
        
        // Create 3 items at launch so we immediately have some data to work with.
        if try! context.fetchCount(FetchDescriptor<Item>()) == 0 {
            for _ in 0..<3 {
                let item = Item(timestamp: Date())
                context.insert(item)
            }
        }
        
        // Now create one unrelated item.
        let unrelatedItem = UnrelatedItem(name: "Mongoose")
        context.insert(unrelatedItem)
        
        try? context.save()
        
        // Set up a background task that updates the unrelated item every second.
        Task {
            while true {
                try? await Task.sleep(nanoseconds: 1_000_000_000)
                Task { @MainActor in
                    // We don't even have to change the name or save the contxt.
                    // Just setting the name to the same value will trigger a change.
                    unrelatedItem.name = "Mongoose"
                }
            }
        }
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(sharedModelContainer)
    }
}

I also added a print statement to the ContentView so I could see when the view updates.

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]

    var body: some View {
        NavigationSplitView {
            List {
                let _ = Self._printChanges()
                ...

The result is that the print statement logs 2 messages to the debug console every second. I checked in iOS 17, 18.1, and 18.2, and they all behave this way.

Is this the intended behavior? I thought the whole point of the new Observation framework in iOS 17 was to track which data had changed and only send change notifications to observers who were using that data.

Answered by DTS Engineer in 825361022

I believe the behavior you described is what the framework currently implemented, and that's because it is hard for SwiftData to determine if a change is really relevant to the result set (Items) with reasonable performance, especially when object graph is complicated.

You are right though that the change on UnrelatedItem is irrelevant and that triggering a SwiftUI view update in this case is unnecessary. I'd hence suggest that you file a feedback report for SwiftData folks to see if any potential improvement can be done – If you do so, please share your report ID here for folks to track.

In a typical case, this doesn't seem to lead to any serious issue because, as you have noticed, SwiftUI is smart enough to determine (based on the attribute graph it maintains) that the view hierarchy under the list is only relevant to Item, and so doesn't go further to update the view hierarchy.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer

I believe the behavior you described is what the framework currently implemented, and that's because it is hard for SwiftData to determine if a change is really relevant to the result set (Items) with reasonable performance, especially when object graph is complicated.

You are right though that the change on UnrelatedItem is irrelevant and that triggering a SwiftUI view update in this case is unnecessary. I'd hence suggest that you file a feedback report for SwiftData folks to see if any potential improvement can be done – If you do so, please share your report ID here for folks to track.

In a typical case, this doesn't seem to lead to any serious issue because, as you have noticed, SwiftUI is smart enough to determine (based on the attribute graph it maintains) that the view hierarchy under the list is only relevant to Item, and so doesn't go further to update the view hierarchy.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thank you for confirming the behavior.

It became a problem for me because my app is constructing a complex view model from my data model, and the frequent updates were using a lot of CPU on the main thread.

Fixing the issue wasn't easy, but I managed it through a combination of design changes:

  • My model objects cache the view model in a @Transient field.
  • My data models notify the view models of changes.
  • The view models provide a CurrentValueSubject for each relevant data property that publishes changes to that property.
  • The view models perform their calculations in the background by subscribing to changes in other view models.
  • The view models are also @Observable, and they modify an observable property when the result of a calculation changes.
  • My SwiftUI views observe the view models for changes.

It was a lot of work to reorganize the code this way, but the app is now performing UI updates only when the relevant data changes, and most of the work is done in the background.

Any SwiftData change updates every SwiftUI view
 
 
Q