我有一个ObservableObject
,它可以执行占用CPU资源的重型工作:
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
}
}
我使用Task
在后台执行计算,使用新的并发特性。这需要使用@MainActor
属性以确保所有UI更新(这里与isComputing
属性相关)都在主actor上执行。
然后,我有以下视图,它显示一个计数器和一个按钮来启动计算:
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
}
}
}
}
问题在于计算似乎会阻塞整个UI:计数器会冻结。
为什么UI会冻结,如何实现`.compute()`以使其不会阻塞UI更新?
我尝试过:
- 将`heavyWork`方法变成async方法,并在每次发布属性更新时散布`await Task.yield()`,这似乎有效,但是这种方法既麻烦又容易出错。而且它只允许一些UI更新,不能在后续的`Task.yield()`调用之间更新UI。 - 如果忽略紫色警告(UI更新应该在主actor上进行),删除`@MainActor`属性似乎可以解决问题(虽然这不是一个有效的解决方案)。
编辑1:
感谢@Bradley提出的答案,我找到了一个预期工作的解决方案(非常接近通常的DispatchQueue方式):
@MainActor
final class Controller: ObservableObject {
@Published private(set) var isComputing: Bool = false
func compute() {
if isComputing { return }
Task.detached {
await MainActor.run {
self.isComputing = true
}
await self.heavyWork()
await MainActor.run {
self.isComputing = false
}
}
}
nonisolated func heavyWork() async {
sleep(5)
}
}
@MainActor
,只需将需要在主线程上运行的部分标记即可。或者将网络工作移动到不是@MainActor
的另一个类中。或者使用DisptachQueue.global().async
将繁重的工作移动到后台,然后使用DispatchQueue.main.async
处理涉及 UI 的部分。 - timbre timbreMainActor.run { }
,这甚至比DispatchQueue
更加模板化。我认为@MainActor
属性只会将状态更新分派到主要执行器,而不是整个方法调用。DispatchQueue
肯定可以工作,但这违背了使用 Swift Concurrency 的初衷。 - Louis Lac