Imagine I have this Model type of class running on a Custom Global Actor:
@GlobalActor
@Observable
class Model {
let channel = AsyncChannel<Int>()
private(set) var count = 0
func click() async {
count += 1
await channel.send(count)
}
}
The examples I'm giving are extremely simplified and could easily be handled without any actor, they're just here to give a minimum problem reproduction.
I would like to consume its updates from a ViewModel running on @MainActor.
I imagine @Observable would be out of the question? At least: I couldn't wrap my head around how to do this between actors.
So I implemented an AsyncChannel and that works really well
I imagine it could even send Model
as it's data type and just publish "there's an update on me" if there would be many fields on this Global Actor-bound class.
The doubts I have started to arise when I tried to have a ViewModel
to consume it's updates and transform them to @Observable fields for my View
:
@MainActor
struct ContentView: View {
@State var viewModel: ViewModel = ViewModel()
var body: some View {
VStack {
Text("Clicked: \(viewModel.clicked)")
Button {
Task {
await viewModel.click()
}
} label: {
Text("Click Me")
}
}
.padding()
}
}
Since the ViewModel
needs to be initialized in a synchronous @MainActor
context I cannot make it's constructor async
nor can I inject the Model
easily as it lives in a different context:
@MainActor
@Observable
class ViewModel {
private(set) var clicked: Int
private var model: Model! // Brittle
init() {
clicked = 0 // Not entirely true
Task {
model = await Container.model // Brittle
clicked = await model.count
Task { @MainActor in
await self.observe(model: model)
}
}
}
func click() async {
await model.click()
}
private func observe(model: Model) async {
for await count in model.channel {
self.clicked = count
}
}
}
I don't like the code in this ViewModel
Since the initializer cannot be async
but the Model
is a hard requirement for this class, I'm now writing code that feels overly brittle. Wouldn't even pass my linter because of the forced unwrap, and making it optional would be a lie as well since it's absolutely elementary to make this class work.
- What would be the go-to pattern for observability between actors?
- Is there any way to make it work more elegant and safe?