Post

Replies

Boosts

Views

Activity

How to execute a CPU-bound task in background using Swift Concurrency without blocking UI updates?
I have an ObservableObject which can do a CPU-bound heavy work: import Foundation import SwiftUI @MainActor final class Controller: ObservableObject { @Published private(set) var isComputing: Bool = false func compute() { if isComputing { return } Task { heavyWork() } } func heavyWork() { isComputing = true sleep(5) isComputing = false } } I use a Task to do the computation in background using the new concurrency features. This requires using the @MainActor attribute to ensure all UI updates (here tied to the isComputing property) are executed on the main actor. I then have the following view which displays a counter and a button to launch the computation: struct ContentView: View { @StateObject private var controller: Controller @State private var counter: Int = 0 init() { _controller = StateObject(wrappedValue: Controller()) } var body: some View { VStack { Text("Timer: \(counter)") Button(controller.isComputing ? "Computing..." : "Compute") { controller.compute() } .disabled(controller.isComputing) } .frame(width: 300, height: 200) .task { for _ in 0... { try? await Task.sleep(nanoseconds: 1_000_000_000) counter += 1 } } } } The problem is that the computation seems to block the entire UI: the counter freezes. Why does the UI freeze and how to implement .compute() in such a way it does not block the UI updates? What I tried Making heavyWork and async method and scattering await Task.yield() every time a published property is updated seems to work but this is both cumbersome and error-prone. Also, it allows some UI updates but not between subsequent Task.yield() calls. Removing the @MainActor attribute seems to work if we ignore the purple warnings saying that UI updates should be made on the main actor (not a valid solution though). Wrapping every state update on a MainActor.run { } but this was too much boilerplate.
1
0
1.3k
Mar ’22