I have two SwiftUI views and a @MainActor protocol for handling navigation events. I’m trying to pass a method reference directly into an escaping closure, but the compiler complains:
Task-isolated ‘self’ is captured by a main actor-isolated closure. main actor-isolated uses in closure may race against later nonisolated uses
import SwiftUI
@MainActor
protocol NavigationHandler {
func onLinkTapped(_ url: URL)
}
struct HostView: View {
private let handler: NavigationHandler
init(
handler: NavigationHandler
) {
self.handler = handler
}
var body: some View {
ContentView(
onTap: handler.onLinkTapped(_:) // ❌ Task-isolated 'self' is captured by a main actor-isolated closure. main actor-isolated uses in closure may race against later nonisolated uses
)
}
}
struct ContentView: View {
let onTap: (URL) -> Void
init(onTap: @escaping (URL) -> Void) {
self.onTap = onTap
}
var body: some View {
Text("Test")
.onTapGesture {
onTap(URL(string: "https://example.com")!)
}
}
}
I am confused because both the onPrivacyPolicyLinkTapped(_ url: URL)
function and the onPrivacyPolicyLinkTapped
closure are MainActor
isolated.
I tried adding @MainActor to the closure definition but the error remained, however if I don’t pass the method reference directly and instead write:
onTap: { url in
handler.onLinkTapped(url)
}
the error goes away.
Why does capturing handler.onLinkTapped(_:) directly triggers the “isolated → non-isolated” error even though the view and protocol are both @MainActor? And Is there any way to pass the method reference directly without wrapping it in a Task or an inline closure?
I am using Xcode 26 beta.3 and swift 6 Note: Code compiles in Xcode 16
I believe this is just a regression in the compiler (similar to this one). The compiler thinks handler.onLinkTapped(_:)
should be sendable. However, it cannot be sendable, because the captured handler
is not sendable.
Many things become implicitly sendable after you put @MainActor
on them. This is perhaps also why the compiler thinks handler.onLinkTapped(_:)
should be sendable (since onLinkTapped
is @MainActor
). However, putting @MainActor
on a protocol doesn't make the protocol sendable. handler
is not sendable.
@MainActor
on a protocol just means the conformer can synchronously use main-actor isolated things in their implementation of the protocol's requirements. This does not promise sendability. I can easily write a conformance that is not sendable.
nonisolated class SomeHandler: NavigationHandler {
var someMutableThing = 0
func onLinkTapped(_ url: URL) {}
}
If NavigationHandler
had been a class or struct, then the compiler would automatically infer that it is Sendable
, since every member of it would be protected by the main actor, no matter where it is sent to.
In any case though, it is not necessary for handler.onLinkTapped(_:)
to be sendable. The inline closure { url in handler.onLinkTapped(url) }
is not sendable, and that's fine. The compiler can see that it is in a main-actor-isolated context, and doesn't escape from the main actor. In other words, it is not being sent to anywhere else. The compiler does produce an error if you do try to send it somewhere else.
// calling f(handler.onLinkTapped(_:)) produces an error
@concurrent func f(_ x: (URL) -> Void) async {}
In addition to putting it in an inline closure, or removing @MainActor
from the protocol, you can also add a Sendable
requirement to the protocol.
@MainActor
protocol NavigationHandler: Sendable {
func onLinkTapped(_ url: URL)
}
This has the side effect of preventing conformers from writing things like SomeHandler
above.