Hi, I have a complex structure of classes, and I'm trying to migrate to swift6
For this classes I've a facade that creates the classes for me without disclosing their internals, only conforming to a known protocol
I think I've hit a hard wall in my knowledge of how the actors can exchange data between themselves. I've created a small piece of code that can trigger the error I've hit
import SwiftUI
import Observation
@globalActor
actor MyActor {
static let shared: some Actor = MyActor()
init() {
}
}
@MyActor
protocol ProtocolMyActor {
var value: String { get }
func set(value: String)
}
@MyActor
func make(value: String) -> ProtocolMyActor {
return ImplementationMyActor(value: value)
}
class ImplementationMyActor: ProtocolMyActor {
private(set) var value: String
init(value: String) {
self.value = value
}
func set(value: String) {
self.value = value
}
}
@MainActor
@Observable
class ViewObserver {
let implementation: ProtocolMyActor
var value: String
init() async {
let implementation = await make(value: "Ciao")
self.implementation = implementation
self.value = await implementation.value
}
func set(value: String) {
Task {
await implementation.set(value: value)
self.value = value
}
}
}
struct MyObservedView: View {
@State var model: ViewObserver?
var body: some View {
if let model {
Button("Loaded \(model.value)") {
model.set(value: ["A", "B", "C"].randomElement()!)
}
} else {
Text("Loading")
.task {
self.model = await ViewObserver()
}
}
}
}
The error
Non-sendable type 'any ProtocolMyActor' passed in implicitly asynchronous call to global actor 'MyActor'-isolated property 'value' cannot cross actor boundary
Occurs in the init on the line "self.value = await implementation.value"
I don't know which concurrency error happens... Yes the init is in the MainActor , but the ProtocolMyActor data can only be accessed in a MyActor queue, so no data races can happen... and each access in my ImplementationMyActor uses await, so I'm not reading or writing the object from a different actor, I just pass sendable values as parameter to a function of the object..
can anybody help me understand better this piece of concurrency problem? Thanks
I’ve boiled your example down to this:
@globalActor
actor MyActor {
static let shared = MyActor()
}
@MyActor
protocol ProtocolMyActor { }
@MyActor
func make(value: String) -> any ProtocolMyActor {
fatalError()
}
@MainActor
class ViewObserver {
init() async {
let implementation = await make(value: "Ciao")
// ^
// Non-sendable type 'any ProtocolMyActor'
// returned by call to global actor
// 'MyActor'-isolated function cannot cross
// actor boundary
print(implementation)
}
}
The issue here is that there are two isolation domains:
-
make(value:)
is isolated to your custom global actor,MyActor
. -
ViewObserver.init()
is isolated to the main actor.
You’re trying to send a value (the resulting any ProtocolMyActor
value) between these two domains, but that value is not sendable.
There at (at least :-) two ways you might fix this:
-
If types conforming to
ProtocolMyActor
must be sendable, you can make that a requirement of the protocol:@MyActor protocol ProtocolMyActor: Sendable { }
-
If you don’t want to add that requirement, you can mark the result of
make(value:)
assending
:func make(value: String) -> sending any ProtocolMyActor {
This adds another constraint though, namely that the
make(value:)
can’t hold on to the values that it returns. Whether that’s a big deal in your case, it’s hard to say without knowing more about the big picture.
Still, this won’t fix your bigger picture problems because, in your larger code snippet, you hit this problem:
self.value = await implementation.value
// ^
// Non-sendable type 'any ProtocolMyActor' passed in
// implicitly asynchronous call to global actor
// 'MyActor'-isolated property 'value' cannot cross
// actor boundary
Again, it’s hard to recommend a fix for that without knowing more about the big picture.
You seem to be painting yourself into a corner here with protocols. What is that protocol for? I suspect you’re using a protocol to mock MyActor
during testing. If so, I recommend that you look for alternative approaches.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"