Search code examples
swiftuiswift-concurrency

How to push state changes out from a SwiftUI Actor


I'm curious, how can we push state changes from an actor out to SwiftUI? For instance, if I made an actor like this:

actor RandomTicker {
    func start() async {
        while true {
            try! await Task.sleep(for: .seconds(Int.random(in: 1...4)))
            // Do something here
        }
    }
}

Ideally, I'd like to be able to change a State variable or object where it says // Do something here, but I'm not sure how to do so. All the example code I've seen is going the other way (ie data being pulled from the actors by the UI, rather than being pushed from the actor). Is there a well-tested solution for this?


Solution

  • Your actors shouldn't be concerned with updating the UI. That's the job of the main actor only.

    It seems appropriate in this case, for start to return an AsyncSequence of Ints. In the SwiftUI view, you can then consume this with a for await loop. Example:

    actor SomeActor {
        
        var number = 1
        
        func start() -> AsyncStream<Int> {
            let (stream, continuation) = AsyncStream.makeStream(of: Int.self)
            let task = Task {
                while true {
                    try await Task.sleep(for: .seconds(.random(in: 1..<5)))
                    print(continuation.yield(number))
                    number += 1
                    try Task.checkCancellation()
                }
            }
            continuation.onTermination = { _ in task.cancel() }
            return stream
        }
    }
    
    .task {
        let someActor = SomeActor()
        for await number in await someActor.start() {
           // suppose someNumberState is a @State
           someNumberState = number
        }
    }
    

    That said, it is technically possible to update a @Published property or a property in an @Observable class. These types are not Sendable, so you need to isolate them to the MainActor, and do all the updating on the MainActor.

    @MainActor
    @Observable
    class SomeState {
        var number = 0
    }
    
    actor SomeActor {
        
        var number = 1
        
        // SomeState is isolated to MainActor, so it is safe to pass it as an argument to SomeActor
        func start(with someState: SomeState) async throws {
            while true {
                try await Task.sleep(for: .seconds(.random(in: 1..<5)))
                let number = self.number
    
                // need to update this on the MainActor
                await MainActor.run {
                    someState.number = number
                }
    
                self.number += 1
                try Task.checkCancellation()
            }
        }
    }