Tuesday, August 26, 2025

SwiftData’s ModelActor Is Just Weird

Matt Massicotte (Mastodon):

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 the ModelContext. 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 access MainActor stuff.

Matt Massicotte:

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.

Rick van Voorden:

AFAIK the legit workaround will continue to be to ensure the ModelActor is created off main. Which leads to workarounds like what we do in ImmutableData sample products when we “box” the ModelActor with a lazy property in another actor that is created on main.

Matt Massicotte:

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:

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.

Leave a Comment