How to access SwiftData from a background thread?

Consider a sample SwiftData project

var body: some View {
	List(recipes) { recipe in
		NavigationLink(recipe.name, destination: RecipeView(recipe))
	}
}

For SwiftUI views that uses SwiftData it's very straight forward.

However, if I need to read/write to SwiftData while in the background (let's say after a network call), the app crashes.

Imagine a simple workflow where

(1) user selects some model objects (via SwiftData modelContext)

(2) now I need to pass these objects to some API server

(3) in a background thread somewhere I need to use the modelActor but it now cannot read/write to the original model objects without risk of crashing

So instead, the only way I thought of is to create a separate modelContext for background processing. There I passed down the container from the main app, and creates a new ModelActor that takes in the container and owns a new modelContext of its own.

This works, without crash. However it introduces many side effects:

(1) read/write from this modelActor seems to trigger view changes for SwiftData's modelContext. But not vice versa.

(2) model objects fetched from one context cannot be used in another context.

(3) changes made in the actor also doesn’t not automatically sync with icloud but changes in SwiftData’s modelContext do.

So I guess the bigger question here is, what is the proper usage of SwiftData in a background thread?

Answered by DTS Engineer in 803321022

Yeah, a SwiftData model object is tied to a model context. If you have an object and would like to use in the other context, use the persistent model ID instead. Concretely:

// In your SwiftUI view:
let modelId = items[0].persistentModelID
Task {
    ...
	try await myModelActor.updateData(identifier: modelId)
}

// In your actor
@ModelActor
public actor MyModelActor {
    public func updateData(identifier: PersistentIdentifier) throws {
        guard let model = modelContext.model(for: identifier) as? ModelActorItem else {
            return
        }
        ... // Change the object here.
        try modelContext.save()
    }
}

Regarding that a change made from a model actor doesn't trigger a SwiftUI update, you might check out the Importing Data into SwiftData in the Background Using ModelActor and @Query post to see if that helps.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Yeah, a SwiftData model object is tied to a model context. If you have an object and would like to use in the other context, use the persistent model ID instead. Concretely:

// In your SwiftUI view:
let modelId = items[0].persistentModelID
Task {
    ...
	try await myModelActor.updateData(identifier: modelId)
}

// In your actor
@ModelActor
public actor MyModelActor {
    public func updateData(identifier: PersistentIdentifier) throws {
        guard let model = modelContext.model(for: identifier) as? ModelActorItem else {
            return
        }
        ... // Change the object here.
        try modelContext.save()
    }
}

Regarding that a change made from a model actor doesn't trigger a SwiftUI update, you might check out the Importing Data into SwiftData in the Background Using ModelActor and @Query post to see if that helps.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

@DTS Engineer Thanks for the reply, some followup questions

With respect to (2):

It seems from the other thread that UI updates could be bugged still. But what is the intended behaviour when it eventually get fixed? Is @Query suppose to update from changes made from other modelContexts? And vice versa? Because if that is not the intended design, and our project requires it, we might be better off going back to core data.

With respect to (3):

Please comment. Is icloud sync suppose to trigger for changes made both in @Query modelContext and ModelActor's custom modelContext? (both pointed to the same container).

Yes, a model context, and hence a query (@Query), is supposed to be able to detect changes from other model contexts and trigger an UI update (if the UI is observing), and the changes include the ones coming from CloudKit synchronization. If you don't see that happening, I'd suggest that you file a feedback report for the SwiftData folks to investigate.

Observing .NSManagedObjectContextDidSave or .NSPersistentStoreRemoteChange and updating UI from there is basically a workaround.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

@DTS Engineer thanks but can you confirm (3)

Do changes made in a background ModelActor trigger a icloud sync?

Accepted Answer

Yes, changes made in any model context, background or not, are supposed to be synchronized to CloudKit, after they are saved, though when the synchronization will happen depends on the system.

Probably worth mentioning, a background model context doesn't do autosave by default, and so you need to save the context explicitly.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

@DTS Engineer thanks and I guess more a generic question, is ModelActor the intended approach to do SwfitData operations in the background? (like the scenario I outlined in the original post). Like am I on the right track?

I don't see anything wrong with regards to using a model actor (ModelActor) in the described case. ModelActor provides an isolated context, just like a normal actor (actor) does, plus a background model context created with the passed-in model maintainer. If you have heavy SwiftData operations that need to be done in the background, you can definitely use it.

Since an actor is isolated, when you need to pass a model object to it and vice versa, you use persistentModelID, as mentioned before.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

This was helpful, I appreciate all the followup responses, thanks

@DTS Engineer

sorry I encounter another architecture question:

I have been studying ModelActor and I'm trying to create one so that I can do SwiftData operations in the background (say after a network call).

But according to this video https://www.youtube.com/watch?v=VG4oCnQ0bfw it seems the ModelActor itself needs to be created in the background off the main thread.

I want to create one actor to be used throughout the app, and not create a new actor for each background operation.

How could I do that? At first, a naive approach might be import Foundation


actor ModelActor {
    // Your ModelActor properties and methods
}

class ModelActorService {
    static let shared = ModelActorService()
    
    private(set) var modelActor: ModelActor?

    private init() {
        // Initialize on a background queue
        DispatchQueue.global(qos: .background).async {
            self.modelActor = ModelActor()
        }
    }
}

// ViewModel example
class SomeViewModel: ObservableObject {
    private let modelActor: ModelActor

    init(modelActor: ModelActor = ModelActorService.shared.modelActor!) {
        self.modelActor = modelActor
    }

    // ViewModel methods using modelActor
}

but that won't work because the creation of the actor is async and there's no guarantee that it would actually be ready when the viewModel wants to use it.

How do I setup a actor facility that is global, created in the background, that can be used by various viewModels for background data operations?

How to access SwiftData from a background thread?
 
 
Q