How to subscribe to an actor's publisher?

In the middle of adapting swift 6, I have converted some of my singleton classes to global actors. Following is one example:

@globalActor actor MyActor: ObservableObject {
    
    static let shared = MyActor()
   
    @Published @MainActor var isBusy: Bool = false
    
     ...

}

In another class, I want to subscribe to the publisher of isBusy variable:

class MyClass: ObservableObject {

    private var cancellables = Set<AnyCancellable>()

    init() {
        Task {
            // Error: Type 'AnyCancellable' does not conform to the 'Sendable' protocol
            // Error: Class 'AnyCancellable' does not conform to the 'Sendable' protocol (Combine.AnyCancellable)
            await MyActor.shared.$isBusy
                .sink {
                    ...
                }
                .store(in: &cancellables)
        }
    }
   ...
}

However, I will get an error for doing this.
I have been trying to create some workaround solutions but none of them works.

What is the recommended way to subscribe to a publisher from an actor or global actor?

1 Like

This is a super-interesting question!

Global actors are a tool for protecting process-global state. And, that's kind of what singletons are, so I see why you reached for them. However, your global actor, from what I can see, doesn't have any of its own internal state to protect. It is really holding onto exclusively MainActor state. And that's why this arrangement is so difficult to get working.

This is kind of a special-case of what I have come to call "split isolation". A type that has different isolation than its properties is typically very problematic. I'm not willing, yet, to say it is always wrong. But, it's something I would strongly caution against unless you have a very clear explanation for what problem it addresses.

Luckily, I think you can really simplify this and get what you need working. ObservableObject subtypes, as you found out, have to be MainActor, because they hold state that must be accessed from the UI. Here's what I'd do:

@MainActor
class NoLongerAnActor: ObservableObject {
    static let shared = NoLongerAnActor()

    @Published var isBusy: Bool = false
}

@MainActor
class MyClass: ObservableObject {
  // ...
}
1 Like

You may find this article Important: Do not use an actor for your SwiftUI data models helpful.

It states:


SwiftUI updates its user interface on the main actor, which means when we make a class conform to ObservableObject we’re agreeing that all our work will happen on the main actor. As an example, any time we modify an @Published property that must happen on the main actor, otherwise we’ll be asking for changes to be made somewhere that isn’t allowed.

2 Likes

Yeah, i think at this point it's worth asking for SwiftUI to update the ObservableObject protocol to prevent this issue. It's just a common problem.

FB15652325

Kinda like how SwiftUI's View protocol was changed in recent SDKs, I'd like to see something similar happen to the ObservableObject protocol. I see no immediate downsides, but they didn't do it, so this might not be straightforward as it seems.

@MainActor // <- add this
protocol ObservableObject : AnyObject {
}
3 Likes

What kind of update would this call for?

Applying MainActor isolation to the protocol, just like what happend to View. I updated my comment with a little more details.

1 Like

Hi @mattie @GreatOm

Thanks for the suggestion. The issue was really the "split isolation".
The original problem was from my app's authenticator which was responsible for managing access tokens, signed in flag and user, etc.

Like you suggested, I seperated my token management into its own actor and made rest of the properties on the @main actor. The problem is then solved.

Thank you very much :sunny:

3 Likes

It's worth noting that ObservableObject and @Published are defined in Combine, not in SwiftUI.

Although 99+% of ObservableObject usage in the wild probably drives UI code in apps, it's perfectly possible AFAIK to use ObservableObject off the main actor for non-UI-related tasks.

2 Likes

Welp this explains why it is the way it is. But I don’t have to like it!

2 Likes