SwiftData’s ModelActor Is Just Weird
So, no doubt there’s lots of historical stuff going on here.
But, that still doesn’t explain how much trouble people have with ModelActor. I’m not sure anyone has ever used
ModelActorwithout at least some surprises.[…]
Actors exist to protect mutable state. The purpose of a
ModelActoris to own and isolate theModelContext. It does that! But if we start to dig into how exactly it does it, we will discover something very bizarre.[…]
Somehow, we are on our custom, minimal, SwiftData-defined actor and also the
MainActorat the same time.[…]
It is bad because consumers of this API have a very reasonable expectation that this will execute off the main thread. This type doesn’t do that. But worse, its relationship with the main thread isn’t visible in the type system. These things are not marked
MainActor, so the compiler doesn’t know what’s going on. This means even though you are on the main thread here, you cannot accessMainActorstuff.
Anyone know if SwiftData’s
ModelActorstill has weird concurrency behavior in OS 26?[…]
Based on some limited testing, no, not fixed.
ModelActortypes can still ultimately execute on the main thread, depending on calling context.
AFAIK the legit workaround will continue to be to ensure the
ModelActoris created offmain. Which leads to workarounds like what we do inImmutableDatasample products when we “box” theModelActorwith alazyproperty in another actor that is created onmain.
Someone proved that init off main is insufficient. I have a theory on what’s happening, and I think this workaround you suggest will always work. But yeah I’m hoping this all just goes away.
Previously:
- Sendable, @unchecked Sendable, @Sendable, sending, and nonsending
- Ways SwiftData’s ModelContainer Can Error on Creation
- SwiftData and Core Data at WWDC25
- @MainActor Not Guaranteed
1 Comment RSS · Twitter · Mastodon
The core issue with ModelActor lies in its implicit behavior — it automatically determines which thread the modelContext runs on (main or background) based on the execution context at creation time. This design introduces unnecessary confusion, particularly for developers who aren't deeply familiar with Swift's concurrency model.
A better approach would be to provide explicit control mechanisms, such as macro parameters that clearly specify whether the ModelActor should run on the main thread or a background thread. This would make the behavior more predictable and transparent.
Another important detail worth noting: even when a ModelActor instance is created on the MainActor, it still creates its own independent modelContext running on the main thread, rather than reusing modelContainer.mainContext. While I believe this is technically the correct design choice (maintaining proper isolation), this behavior can indeed be counterintuitive for some developers.
Testing on Xcode 16 beta 7 shows that ModelActor now correctly selects the appropriate execution thread based on its creation context. More importantly, it can now properly respond to data updates from non-main thread contexts, which is a significant improvement.
Ultimately, the root of the problem is insufficient documentation clarity and a lack of explicit control in the API design.