我有一门ObservableObject课和一个快捷界面.当点击一个按钮时,我会创建一个Task,并从中调用populate(一个异步函数).我原以为这会在后台线程上执行populate,但整个UI会冻结.这是我的代码:

class ViewModel: ObservableObject {
    @Published var items = [String]()
    func populate() async {
        var items = [String]()
        for i in 0 ..< 4_000_000 { /// this usually takes a couple seconds
            items.append("\(i)")
        }
        self.items = items
    }
}

struct ContentView: View {
    @StateObject var model = ViewModel()
    @State var rotation = CGFloat(0)

    var body: some View {
        Button {
            Task {
                await model.populate()
            }
        } label: {
            Color.blue
                .frame(width: 300, height: 80)
                .overlay(
                    Text("\(model.items.count)")
                        .foregroundColor(.white)
                )
                .rotationEffect(.degrees(rotation))
        }
        .onAppear { /// should be a continuous rotation effect
            withAnimation(.easeInOut(duration: 2).repeatForever()) {
                rotation = 90
            }
        }
    }
}

结果:

Rotation animation freezes when the button is pressed

按钮停止移动,然后在populate结束时突然弹回来.

奇怪的是,如果我把Task移动到populate本身,go 掉async,旋转动画就不会断断续续,所以我认为循环实际上是在后台执行的.然而,我现在得到了Publishing changes from background threads is not allowed的警告.

func populate() {
    Task {
        var items = [String]()
        for i in 0 ..< 4_000_000 {
            items.append("\(i)")
        }
        self.items = items /// Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
    }
}

/// ...

Button {
    model.populate()
}

结果:

Rotation animation continues even when the button is pressed

如何确保我的代码在后台线程上执行?我想这可能与MainActor有关,但我不确定.

推荐答案

首先,你不能两全其美;要么在主线程上执行CPU密集型工作(并对UI产生负面影响),要么在另一个线程上执行工作,但需要显式地将UI更新分派到主线程上.

然而,你真正想问的是

(通过使用Task)我以为这会在后台线程上执行,但整个UI会冻结.

当你使用Task时,你使用的是unstructured concurrency,当你通过init(priority:operation) the task ... inherits the priority and actor context of the caller初始化你的Task时.

虽然Task是异步执行的,但它使用调用者的参与者上下文来执行,在View body的上下文中,参与者上下文是主要参与者.这意味着,虽然任务是异步执行的,但它仍然在主线程上运行,并且该线程在处理时不可用于UI更新.你是对的,这和MainActor有关.

当您将Task移动到populate时,它不再在MainActor上下文中创建,因此不会在主线程上执行.

正如您所发现的,您需要使用第二种方法来避免主线程.对代码所需要做的就是使用MainActor:

func populate() {
    Task {
        var items = [String]()
        for i in 0 ..< 4_000_000 {
            items.append("\(i)")
        }
        await MainActor.run {
            self.items = items 
        }
    }
}

你也可以在body上下文中使用Task.detached()来创建一个Task,这个Task没有附加到MainActor上下文中.

Ios相关问答推荐

如何在进行滑动删除操作时保持自定义视图背景静态

自定义alert 未执行任何操作

第三方框架的iOS隐私声明

iOS中的分段拾取器—手柄点击已 Select 的项目

UIImage大小调整工作,但转换为上传数据时恢复到原始大小

阿拉伯文变音符号(Harakat)在文本对齐的UILabel中未对齐

在金属中设置纹理的UV坐标以编程方式变形纹理

RFID scanner 的蓝牙服务 UUID?

Swift Combine和iOS版本限制?

NavigationLink 只能在文本部分点击

为什么@FetchRequest 在删除时不更新和重绘视图?

在 SwiftUI 中重构提取到子视图的 GeometryReader 代码

SwiftUI 中的偏移量和按钮有问题吗?

如何将单个按钮传递到自定义 ConfirmationDialog

UIScrollview 获取touch 事件

如何使用 Swift 从assets资源 中加载特定图像

如何动态计算 UILabel 高度?

如何删除UITableView中最后一个单元格的最后一个边框?

打开 XIB 文件后 Xcode 6.3 冻结/挂起

openURL:在 iOS 10 中已弃用