While Swift has offered strict concurrency checking for some time, many of Apple’s official APIs have yet to be fully adapted, and this situation may persist for quite a while. 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
.
An Email
A few days ago, I received an email from a fellow developer, Lucas. He encountered a problem where legacy APIs couldn’t meet Swift 6 compilation requirements. He wanted to implement custom view insertion in UITextView
using NSTextAttachment + NSTextAttachmentViewProvider
. To achieve this, he tried loading a SwiftUI view in the loadView
method of NSTextAttachmentViewProvider
:
class CustomAttachmentViewProvider: NSTextAttachmentViewProvider {
override func loadView() {
let hosting = UIHostingController(rootView: InlineSwiftUIButton {
print("SwiftUI Button tapped!")
})
hosting.view.backgroundColor = .clear
// Assign to the provider's view
self.view = hosting.view
}
}
// MARK: - SwiftUI Button View
struct InlineSwiftUIButton: View {
var action: () -> Void
var body: some View {
Button("Click Me") {
action()
}
.padding(6)
.background(Color.blue.opacity(0.2))
.cornerRadius(8)
}
}
After enabling Swift 6 mode in Xcode (with Default Actor Isolation set to nonisolated
), the above code produced the following errors/warnings:
To solve this compilation issue, Lucas tried various approaches:
- Adding
@MainActor
to theloadView
method
- Adding
@MainActor
to theCustomAttachmentViewProvider
class
- Wrapping the code in
loadView
within aTask
However, none of these methods satisfied the Swift compiler’s requirements.
In Xcode 26 beta 5, the original code produces errors (compilation fails), while in beta 6 it generates warnings (but compiles).
Does this mean such legacy APIs cannot achieve perfect compilation in Swift 6 targets (without warnings or errors)?
Analyzing the Problem
The Swift 6 compiler rejects these approaches for the following reasons:
UIHostingController
is annotated with@MainActor
in its declaration, meaning it must be created in a MainActor contextNSTextAttachmentViewProvider
’s original declaration has no explicit isolation- Adding
@MainActor
solely toloadView
doesn’t match the parent class requirements - If we build a MainActor async context within
loadView
, we cannot safely passself
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
.
Is there a way to satisfy both requirements?
MainActor.assumeIsolated: Providing MainActor Context in Synchronous Methods
Among Swift’s concurrency APIs, there’s a rather peculiar one: MainActor.assumeIsolated
. Unlike MainActor.run
, it can only run in a synchronous context, and if the current context isn’t MainActor
, the app will crash immediately.
When I first encountered this method, I was puzzled about its purpose. For a long time, I only used it like MainActor.assertIsolated
, as a debugging tool to determine whether the current context was MainActor
. It wasn’t until I encountered the problem described in this article that I truly understood this API’s design intent.
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.
public static func assumeIsolated<T>(_ operation: @MainActor () throws -> T, file: StaticString = #fileID, line: UInt = #line) rethrows -> T where T : Sendable
The Solution
Understanding how MainActor.assumeIsolated
works, we can rewrite loadView
as:
class CustomAttachmentViewProvider: NSTextAttachmentViewProvider {
override func loadView() {
let view = MainActor.assumeIsolated { // Running in synchronous context
// assumeIsolated closure provides MainActor environment, allowing safe UIHostingController creation
let hosting = UIHostingController(rootView: InlineSwiftUIButton {
print("SwiftUI Button tapped!")
})
hosting.view.backgroundColor = .clear
return hosting.view // view is UIView, annotated with MainActor, satisfies Sendable
}
self.view = view
}
}
In the code above:
- We successfully execute
MainActor.assumeIsolated
withinloadView
- The
MainActor.assumeIsolated
closure provides a MainActor context, allowing us to safely create aUIHostingController
instance hosting.view
is of typeUIView
(already annotated with@MainActor
in its declaration), satisfying theSendable
requirement, and can be returned from the closure- In the synchronous context of
loadView
, we assign the return value ofMainActor.assumeIsolated
toself.view
, ensuring isolation consistency
Considering that loadView
doesn’t always execute in MainActor
, here’s the complete final code:
class CustomAttachmentViewProvider: NSTextAttachmentViewProvider {
override func loadView() {
view = getView()
}
// Switch to main thread if `loadView` isn't running on main thread
func getView() -> UIView {
if Thread.isMainThread {
return Self.createHostingViewOnMain()
} else {
return DispatchQueue.main.sync {
Self.createHostingViewOnMain()
}
}
}
// Using static method to avoid capturing self
private static func createHostingViewOnMain() -> UIView {
MainActor.assumeIsolated {
let hosting = UIHostingController(rootView: InlineSwiftUIButton {
print("SwiftUI Button tapped!")
})
hosting.view.backgroundColor = .clear
return hosting.view
}
}
}
With this, we’ve implemented a solution that fully satisfies Swift 6 compiler requirements and is compatible with legacy APIs.
Perhaps Not the Most Elegant, but Its Existence Is Justified
To catch concurrency issues at compile time, Swift has added numerous related keywords and methods in recent years, which has increased developers’ learning costs and usage difficulty to some extent. While I’m not well-versed in programming language design, intuitively, this approach of piling on features doesn’t seem elegant and adds to comprehension difficulty.
However, considering that Swift doesn’t exist in isolation—in practice, we need to work with numerous legacy APIs and even Objective-C code—these seemingly “cumbersome” designs do have their justification.
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.
If this article helped you, feel free to buy me a coffee ☕️ . For sponsorship inquiries, please check out the details here.