Search code examples
swiftconcurrencyswiftui

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


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?


Solution

  • The accepted answer might not suite all use cases.

    In my case, I want the ObservableObject to remain an actor, but access one of its property from a SwiftUI view. So I added @MainActor only to that property.

    This means that property also needs to be updated only from the main thread as shown below.

    actor Model: ObservableObject {
      @MainActor @Published var num: Int = 0
    
      func updateNumber(_ newNum: Int) {
        Task { @MainActor in 
          self.num = newNum
        }
      }
    
      // all other properties and functions will remain isolated to the actor
    }