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
ModelActor
without at least some surprises.[…]
Actors exist to protect mutable state. The purpose of a
ModelActor
is 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
MainActor
at 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 accessMainActor
stuff.
Anyone know if SwiftData’s
ModelActor
still has weird concurrency behavior in OS 26?[…]
Based on some limited testing, no, not fixed.
ModelActor
types can still ultimately execute on the main thread, depending on calling context.
AFAIK the legit workaround will continue to be to ensure the
ModelActor
is created offmain
. Which leads to workarounds like what we do inImmutableData
sample products when we “box” theModelActor
with alazy
property 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.