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()
}
}
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.