Accessing an actor's isolated state from within a SwiftUI view

Viewed 1382

I'm trying to understand a design pattern for accessing the isolated state held in an actor type from within a SwiftUI view.

Take this naive code:

actor Model: ObservableObject {
    @Published var num: Int = 0
    
    func updateNumber(_ newNum: Int) {
        self.num = newNum
    }
}

struct ContentView: View {
    @StateObject var model = Model()
    
    var body: some View {
        Text("\(model.num)") // <-- Compiler error: Actor-isolated property 'num' can not be referenced from the main actor
        Button("Update number") {
            Task.detached() {
                await model.updateNumber(1)
            }
        }
    }
}

Understandably I get the compiler error Actor-isolated property 'num' can not be referenced from the main actor when I try and access the isolated value. Yet I can't understand how to display this data in a view. I wonder if I need a ViewModel that observes the actor, and updates itself on the main thread, but get compile time error Actor-isolated property '$num' can not be referenced from a non-isolated context.

class ViewModel: ObservableObject {
    
    let model: Model
    @Published var num: Int
    let cancellable: AnyCancellable
    
    init() {
        let model = Model()
        self.model = model
        self.num = 0
        self.cancellable = model.$num
            .receive(on: DispatchQueue.main)
            .sink { self.num = $0 }
    }
    
}

Secondly, imagine if this code did compile, then I would get another error when clicking the button that the interface is not being updated on the main thread...again I'm not sure how to effect this from within the actor?

1 Answers

You need to use MainActor on your view model (named Model) indicating that all dispatching in it be on main thread (of course assuming you will use new swift concurrency system for it).

So it would look like

@MainActor 
class Model: ObservableObject {
    @Published var num: Int = 0

    func updateNumber(_ newNum: Int) {
        self.num = newNum
    }
}

Note: I'd recommend to make all updates inside class, because someone in some place might forget to update it in right way.

Related