SwiftData updates in the background are not merged in the main UI context

Hello,

SwiftData is not working correctly with Swift Concurrency. And it’s sad after all this time.

I personally found a regression. The attached code works perfectly fine on iOS 17.5 but doesn’t work correctly on iOS 18 or iOS 18.1. A model can be updated from the background (Task, Task.detached or ModelActor) and refreshes the UI, but as soon as the same item is updated from the View (fetched via a Query), the next background updates are not reflected anymore in the UI, the UI is not refreshed, the updates are not merged into the main.

How to reproduce:

  • Launch the app
  • Tap the plus button in the navigation bar to create a new item
  • Tap on the “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times
  • Notice the time is updated
  • Tap on the “Update from View” (once or many times)
  • Notice the time is updated
  • Tap again on “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times
  • Notice that the time is not update anymore

Am I doing something wrong? Or is this a bug in iOS 18/18.1?

Many other posts talk about issues where updates from background thread are not merged into the main thread. I don’t know if they all are related but it would be nice to have

1/ bug fixed, meaning that if I update an item from a background, it’s reflected in the UI, and 2/ proper documentation on how to use SwiftData with Swift Concurrency (ModelActor). I don’t know if what I’m doing in my buttons is correct or not.

Thanks, Axel

import SwiftData
import SwiftUI

@main
struct FB_SwiftData_BackgroundApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .modelContainer(for: Item.self)
        }
    }
}

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    
    @State private var simpleModelActor: SimpleModelActor!
    @Query private var items: [Item]
    
    var body: some View {
        NavigationView {
            VStack {
                if let firstItem: Item = items.first {
                    Text(firstItem.timestamp, format: Date.FormatStyle(date: .omitted, time: .standard))
                        .font(.largeTitle)
                        .fontWeight(.heavy)
                    
                    Button("Update from Task") {
                        let modelContainer: ModelContainer = modelContext.container
                        let itemID: Item.ID = firstItem.persistentModelID
                        
                        Task {
                            let context: ModelContext = ModelContext(modelContainer)
                            
                            guard let itemInContext: Item = context.model(for: itemID) as? Item else { return }
                            
                            itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))

                            try context.save()
                        }
                    }
                    .buttonStyle(.bordered)
                    
                    Button("Update from Detached Task") {
                        let container: ModelContainer = modelContext.container
                        let itemID: Item.ID = firstItem.persistentModelID
                        
                        Task.detached {
                            let context: ModelContext = ModelContext(container)
                            
                            guard let itemInContext: Item = context.model(for: itemID) as? Item else { return }
                            
                            itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
                            
                            try context.save()
                        }
                    }
                    .buttonStyle(.bordered)
                    
                    Button("Update from ModelActor") {
                        let container: ModelContainer = modelContext.container
                        let persistentModelID: Item.ID = firstItem.persistentModelID
                        
                        Task.detached {
                            let actor: SimpleModelActor = SimpleModelActor(modelContainer: container)
                            await actor.updateItem(identifier: persistentModelID)
                        }
                    }
                    .buttonStyle(.bordered)
                    
                    Button("Update from ModelActor in State") {
                        let container: ModelContainer = modelContext.container
                        let persistentModelID: Item.ID = firstItem.persistentModelID
                        
                        Task.detached {
                            let actor: SimpleModelActor = SimpleModelActor(modelContainer: container)
                            
                            await MainActor.run {
                                simpleModelActor = actor
                            }
                            
                            await actor.updateItem(identifier: persistentModelID)
                        }
                    }
                    .buttonStyle(.bordered)
                    
                    Divider()
                        .padding(.vertical)
                
                    Button("Update from View") {
                        firstItem.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
                    }
                    .buttonStyle(.bordered)
                } else {
                    ContentUnavailableView(
                        "No Data",
                        systemImage: "slash.circle", // 􀕧
                        description: Text("Tap the plus button in the toolbar")
                    )
                }
            }
            .toolbar {
                ToolbarItem(placement: .primaryAction) {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        }
    }
    
    private func addItem() {
        modelContext.insert(Item(timestamp: Date.now))
        try? modelContext.save()
    }
}

@ModelActor
final actor SimpleModelActor {
    var context: String = ""

    func updateItem(identifier: Item.ID) {
        guard let item = self[identifier, as: Item.self] else {
            return
        }

        item.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
        
        try! modelContext.save()
    }
}

@Model
final class Item: Identifiable {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}
SwiftData updates in the background are not merged in the main UI context
 
 
Q