Friday, November 7, 2025

MainActor.assumeIsolated, Preconcurrency, and Isolated Conformances

Fatbobman:

As Swift 6 gradually gains adoption, this problem becomes increasingly prominent: developers want to benefit from the concurrency safety guarantees provided by the Swift compiler, while struggling with how to make their code meet compilation requirements. This article will demonstrate the clever use of MainActor.assumeIsolated in specific scenarios through an implementation case with NSTextAttachmentViewProvider.

[…]

We seem to be caught in a dilemma: we need to construct UIHostingController in MainActor, yet we cannot assign the constructed view (UIView) to self.view within MainActor.

[…]

Looking at MainActor.assumeIsolated’s signature, we can see that this API provides a MainActor context for its trailing closure. This means we can “synchronously” run code that can only execute in a MainActor context within a non-MainActor synchronous context, without creating an async environment, and return a Sendable result.

[…]

I still hope we can move past this somewhat “chaotic” transition period soon. Perhaps in a few years, when numerous official and third-party frameworks have completed their Swift 6 migration, we’ll finally enjoy a more relaxed safe concurrent programming experience.

Matt Massicotte:

I consistently find the @preconcurrency attribute to be confusing. But, I’m tired of that. Let’s just, once and for all, get a better handle how to use this thing.

[…]

It has three distinct uses. And while they all apply to definitions, the details are quite different.

Jesse Squires:

UIKit provides two diffable data source APIs, one for collections and one for tables. Recently, while working on ReactiveCollectionKit, I noticed that the APIs were updated for Swift Concurrency in the iOS 18 SDK, but the annotations were inconsistent with the documentation.

[…]

I reached out to Tyler Fox from the UIKit team on Mastodon to ask if this was a mistake. As it turns out, it is not a mistake and his reply was incredibly helpful and insightful. For posterity and documentation purposes (and because social media is ephemeral and unreliable), I’m going to reproduce his entire response here[…]

Wade Tregaskis:

These might seem pretty similar – you’d be forgiven for assuming it’s just a convenience to put @MainActor on the protocol overall rather than having to repeat it for every member of the protocol. Less error-prone, too.

But, you generally shouldn’t do that. They are not equivalent.

The first form is not merely saying that all the members of the protocol require a certain isolation, but that the type that conforms to the protocol must have that isolation. The whole type.

Matt Massicotte:

Further, as far as the compiler is concerned, there is an actor boundary both going into and returning from assumeIsolated. This means you cannot work with non-Sendable data here and that can be an enormous pain.

[…]

Before Swift 6.0, dynamic isolation was the only option. And before Swift 6.2, I think that preconcurrency conformances were the best tool for handling protocol isolation mismatches. They address pretty much all of the weakness of the nonisolated-assumeIsolated thing. But they just feel funny.

[…]

Swift 6.2 allows us to express this idea directly, by constraining a conformance to be valid only for a particular global actor.

What we now have is exactly what we want. A MainActor type that is Equatable in that context only. This is not the same as a true, unconstrainted Equatable, because those work everywhere. It’s a little like defining a new, special variant of that protocol right in line at the conformance declaration site.

[…]

But remember, not all protocols are compatible. And making this entire thing implicit makes the problems even more surprising. Don’t get me wrong, I really like isolated conformances and am very happy to see them come to the language. But they are not a magic bullet (and neither is MainActor-by-default).

Lukas Valenta at mDevCamp (Mastodon):

The talk focuses - as the name suggests - to strategy to migrate the project from Swift 5 compilation mode to Swift 6. We will discuss several issues anyone will encounter to have project that compiles under Swift 6 mode, such as issues with Combine and Async publishers, DispatchQueue.main precondition queue checks, working with older APIs that predate the concurrency, as well as a debate whether it is all worth it. I will also mention the transition of not only the project itself but also a story of external dependencies, some of which written by me, and how did the migration in the libraries took place.

Previously:

6 Comments RSS · Twitter · Mastodon


The commentary on DiffableDataSource turning main-threaded makes me think the hardware guys will solve a lot of performance issues before the Swift guys can figure out an app concurrency model that makes sense.

I read the above and I continue to not get it. It makes me think the "islands of concurrency" talk is old thinking and Concurrency is going back into the "break glass in case of [performance] emergency" box. I just never want to touch this stuff, which is the strongest "concurrency safety guarantee" you can get.


I've never used a diffable datasource on a background queue. I think I'd just avoid using one altogether if the amount of data was so much that a background queue was required in order to diff it without killing performance. So what Tyler said makes sense but to just take the option off the table to me that sounds like "we couldn't really figure out how to do this -so we just kind of gave up." Will this be the case with the zillions of other APIs?

> I still hope we can move past this somewhat “chaotic” transition period soon.

If they continue down this road I think there will be a transition period.... a transition period where devs flock to whatever non-Apple programming language/UI framework around that isn't utterly insane.


Just to add aren’t situations like loadView being called off the main thread…isn’t that what the Main Thread Checker is for?

I don’t think asyncAndWait then fetching the view on the main queue and returning it back to the caller on whatever queue it’s on looks like a good idea. you’re supposed to crash right?

So because something isn’t annotated just take a semaphore and fetch from the main queue from your background queue and wait? This all looks backwards and fucked up but the things you gotta do to compile Swift…

So ya’ll don’t want header files but you want this? Wtf is wrong with you ppl 😂


https://developer.apple.com/forums/thread/806619

This all looks like good fun …simple subclassing has turned into a puzzle.

How come Xcode can’t suggest the fix? Is anyone at Apple embarrassed by this? They should be


@Anonymous I kind of expected that NSMenuItem would be @MainActor. But given that it’s not, this seems like a straightforward violation of the substitution principle, not really a concurrency-specific issue. I agree that Xcode should suggest a fix, though.


@Anonymous There’s more information about this particular issue on the Swift Forum.

Leave a Comment