Search code examples
swiftswiftuicore-dataswiftdata

View Not Updating When Using Background Actor to Write Data to Container


I’m currently studying SwiftData I want to write and save data to my container application from the ContentView and at the same time using modelActor I also want to save from background thread.

Base on my understanding, either way , should update my View when change happens, but this do not happen when I save data from the background.

What is wrong on my code:

//
//  ContentView.swift
//  Test-SwiftData
//
//  Created by Damiano Miazzi on 01/08/2024.
//

import SwiftUI
import SwiftData

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

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
                    } label: {
                        HStack{
                            Text(item.text)
                        }
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
                ToolbarItem {
                    Button {
                        Task{
                            let act = DataManager(modelContainer: modelContext.container)
                            await act.insert()
                        }
                    } label: {
                        Text("Add BK")
                    }

                }
            }
        } detail: {
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date(), text: "Main")
            modelContext.insert(newItem)
            try? modelContext.save()
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(items[index])
            }
        }
    }
}


@Model
final class Item {
    var timestamp: Date
    var text: String
    
    init (timestamp: Date = Date(), text: String) {
        self.timestamp = timestamp
        self.text = text
    }
}
@main
struct Test_SwiftDataApp: App {
    var sharedModelContainer: ModelContainer = {
        let schema = Schema([
            Item.self,
        ])
        let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)

        do {
            return try ModelContainer(for: schema, configurations: [modelConfiguration])
        } catch {
            fatalError("Could not create ModelContainer: \(error)")
        }
    }()

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



@ModelActor
actor DataManager {
    
    @MainActor
    public init(modelContainer: ModelContainer,mainActor _: Bool) {
        let modelContext = modelContainer.mainContext
        modelExecutor = DefaultSerialModelExecutor(modelContext: modelContext)
        self.modelContainer = modelContainer
    }
    
    func insert(){
        let item = Item(timestamp: Date(), text: "BK")
        modelContext.insert(item)
        try? modelContext.save()
    }
    
}


Solution

  • We are facing the same issue, after investigating it for few days it seems to be a problem with iOS 18 and SwiftData, according to this https://developer.apple.com/forums/thread/759364 a fix is being made, below you will find the temporal workaround suggested by Apple engineer:

    For a workaround before the issue is fixed on the framework side, you might consider observing .NSManagedObjectContextDidSave notification and triggering a SwiftUI update from the notification handler. For example:

    import Combine
    
    extension NotificationCenter {
        var managedObjectContextDidSavePublisher: Publishers.ReceiveOn<NotificationCenter.Publisher, DispatchQueue> {
            return publisher(for: .NSManagedObjectContextDidSave).receive(on: DispatchQueue.main)
        }
    }
    
    struct MySwiftDataView: View {
        @Query private var items: [Item]
        
        // Use the notification time as a state to trigger a SwiftUI update.
        // Use a state more appropriate to your app, if any.
        @State private var contextDidSaveDate = Date()
        
        var body: some View {
            List {
                ForEach(items) { item in
                    Text("\(item.timestamp)")
                }
                // Refresh the view by changing its `id`.
                // Use a way more appropriate to your app, if any.
                .id(contextDidSaveDate)
            }
            .onReceive(NotificationCenter.default.managedObjectContextDidSavePublisher) { _ in
                contextDidSaveDate = .now
            }
        }
    }
    

    This will refresh every view you attach the contextDidSaveDate to as .id(contextDidSaveDate) when a context save() is being performed.