SwiftData thread-safety: passing models between threads

Hello,

I'm trying to understand how dangerous it is to read and/or update model properties from a thread different than the one that instantiated the model.

I know this is wrong when using Core Data and we should always use perform/performAndWait before manipulating an object but I haven't found any information about that for SwiftData.

Question: is it safe to pass an object from one thread (like MainActor) to another thread (in a detached Task for example) and manipulate it, or should we re fetch the object using its persistentModelID as soon as we cross threads?

When running the example app below with the -com.apple.CoreData.ConcurrencyDebug 1 argument passed at launch enabled, I don't get any Console warning when I tap on the "Update directly" button. I'm sure it would trigger a warning if I were using Core Data.

Thanks in advance for explaining. Axel

--

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

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

    var body: some View {
        VStack {
            Button("Add") {
                context.insert(Item(timestamp: Date.now))
            }
            
            if let firstItem = items.first {
                Button("Update directly") {
                    Task.detached {
                        // Not the main thread, but firstItem is from the main thread
                        // No warning in Xcode
                        firstItem.timestamp = Date.now
                    }
                }
                
                Button("Update using persistentModelID") {
                    let container: ModelContainer = context.container
                    let itemIdentifier: Item.ID = firstItem.persistentModelID
                    
                    Task.detached {
                        let backgroundContext: ModelContext = ModelContext(container)
                        guard let itemInBackgroundThread: Item = backgroundContext.model(for: itemIdentifier) as? Item else { return }
                        
                        // Item on a background thread
                        itemInBackgroundThread.timestamp = Date.now
                        
                        try? backgroundContext.save()
                    }
                }
            }
        }
    }
}

@Model
final class Item: Identifiable {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

You should pass the id (persistentModelID), as you do in your code above, across actor boundaries and never the object itself.

SwiftData thread-safety: passing models between threads
 
 
Q