我有一个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属性).

然后,我有以下视图,其中显示了一个计数器和一个启动计算的按钮:

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:计数器冻结.

Why does the UI freeze and how to implement 100 in such a way it does not block the UI updates?


我试过的

  • 每次更新发布的属性时,使用heavyWork和异步方法以及await Task.yield()似乎都有效,但这既麻烦又容易出错.此外,它允许some次用户界面更新,但不能在随后的Task.yield()次调用之间进行.
  • 如果我们忽略紫色警告,即应该对主参与者进行UI更新,那么删除@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)
    }
}

推荐答案

问题是heavyWorkController继承了MainActor隔离,这意味着工作将在主线程上执行.

我们需要确保(1)以较低的优先级运行繁重的工作,这样系统就不太可能在UI线程上调度它.

然后(2),我们确保heavyWorknonisolated,所以它不会从Controller继承@MainActor上下文.

然后(3),我们等待使用"任务句柄"返回的value属性计算值.

@MainActor
final class Controller: ObservableObject {
    @Published private(set) var isComputing: Bool = false
    
    func compute() {
        if isComputing { return }
        Task {
            isComputing = true

            // (1) run detached at a non-UI priority
            let work = Task.detached(priority: .low) {
                self.heavyWork()
            }

            // (3) non-blocking wait for value
            let result = await work.value
            print("result on main thread", result)

            isComputing = false
        }
    }
    
    // (2) will not inherit @MainActor isolation
    nonisolated func heavyWork() -> String {
        sleep(5)
        return "result of heavy work"
    }
}

Swift相关问答推荐

如何在visionOS上删除PhotosPicker背景 colored颜色 ?

OBJC代码中Swift 演员的伊瓦尔:原子还是非原子?

如果通过计时器循环运行,则检索CPU利用率百分比的SWIFT脚本运行良好.似乎在没有计时器的情况下停留在初始百分比上

如何使用AsyncTimerSequence获取初始时钟,然后在指定的时间间隔内开始迭代?

文件命名&NumberForMatter+扩展名&:含义?

如何写一个;风格;视图修饰符,它会影响特定类型的所有嵌套视图?

如何将多个完成处理程序转换为异步?

TimeZone 背后的目的

如何判断设备是否为 Vision Pro - Xcode 15 Beta 4

如何在visionOS中将模型锚定在用户头部上方?

如何删除快速处理消息

URL appendPathComponent(_:) 已弃用?

如何在不将变量转换为字符串的情况下判断变量在 Swift 中是否为 Optional(nil)?

我如何遵守与演员的协议?

是否可以使任何协议符合 Codable?

为什么 Swift Tuples 只能在元素个数小于等于 6 的情况下进行比较?

如何从元组数组创建字典?

Swift 2.0 中的 do { } catch 不处理从这里抛出的错误

滑动侧边栏菜单 IOS 8 Swift

如何在 Swift 中从字符串创建类的实例