Working correctly with actor annotated class

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

Answered by DTS Engineer in 814411022

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:) as sending:

    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"

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:) as sending:

    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"

Working correctly with actor annotated class
 
 
Q