以下是我的观点:

import SwiftUI

struct ContentView: View {
    private let weatherLoader = WeatherLoader()
    
    @State private var temperature = ""
    @State private var pressure = ""
    @State private var humidity = ""
    @State private var tickmark = ""
    @State private var refreshable = true
    
    var body: some View {
        GeometryReader { metrics in
            VStack(spacing: 0) {
                Grid(horizontalSpacing: 0, verticalSpacing: 0) {
                    GridRow {
                        Text("Температура")
                            .frame(width: metrics.size.width/2)
                        Text("\(temperature) °C")
                            .frame(width: metrics.size.width/2)
                    }.frame(height: metrics.size.height*0.8*0.25)
                    
                    GridRow {
                        Text("Давление")
                            .frame(width: metrics.size.width/2)
                        Text("\(pressure) мм рт ст")
                            .frame(width: metrics.size.width/2)
                    }.frame(height: metrics.size.height*0.8*0.25)

                    GridRow {
                        Text("Влажность")
                            .frame(width: metrics.size.width/2)
                        Text("\(humidity) %")
                            .frame(width: metrics.size.width/2)
                    }.frame(height: metrics.size.height*0.8*0.25)

                    GridRow {
                        Text("Дата обновления")
                            .frame(width: metrics.size.width/2)
                        Text("\(tickmark)")
                            .frame(width: metrics.size.width/2)
                    }.frame(height: metrics.size.height*0.8*0.25)
                }.frame(height: metrics.size.height*0.8)
                
                Button("Обновить") {
                    refreshable = false
                    print("handler : \(Thread.current)")
                    Task.detached {
                        print("task : \(Thread.current)")
                        let result = await weatherLoader.loadWeather()
                        await MainActor.run {
                            print("main actor: \(Thread.current)")
                            switch result {
                            case .success(let item):
                                temperature = item.temperature
                                pressure = item.pressure
                                humidity = item.humidity
                                tickmark = item.date
                            case .failure:
                                temperature = ""
                                pressure = ""
                                humidity = ""
                                tickmark = ""
                            }
                            
                            refreshable = true
                        }
                    }
                }
                .disabled(!refreshable)
                .padding()
            }
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .frame(width: 320, height: 240)
    }
}

问题是,从异步上下文中更新@State个变量的正确方式是什么?我看到如果我go 掉MainActor.run没有失败,但是在处理UIKit时,我们必须从主线程调用这个更新.这里有什么不同吗?我还了解到,任务继承了MainActor上下文,因此我放置了Task.detached以确保它是Main之外的另一个线程.有谁能给我讲清楚吗?

推荐答案

如果使用以下命令运行任务

Task { @MainActor in
    //
}

则任务本身内的代码将在主队列上运行,但是任何异步调用it可以在任何队列上运行.

添加WeatherLoader的实现如下:

class WeatherLoader {
    
    func loadWeather() async throws -> Item {
        print("load : \(Thread.current)")
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return Item()
    }
}

然后打电话,就像这样:

print("handler : \(Thread.current)")
Task { @MainActor in
    print("task : \(Thread.current)")
    do {
        let item = try await weatherLoader.loadWeather()
        print("result : \(Thread.current)")
        temperature = item.temperature
        pressure = item.pressure
        humidity = item.humidity
        tickmark = item.date
    } catch {
        temperature = ""
        pressure = ""
        humidity = ""
        tickmark = ""
    }
        
    refreshable = true
}

您将看到类似于

handler : <_NSMainThread: 0x6000000f02c0>{number = 1, name = main}
task : <_NSMainThread: 0x6000000f02c0>{number = 1, name = main}
load : <NSThread: 0x6000000a1e00>{number = 6, name = (null)}
result : <_NSMainThread: 0x6000000f02c0>{number = 1, name = main}

如您所见,handlertask在主队列上运行,load在其他队列上运行,然后result在主队列上运行. 由于任务本身中的代码保证在主队列上运行,因此从那里更新状态变量是安全的.

正如您在上面提到的,在这种情况下,@MainActor in实际上不是必需的,Task(priority:operation:)继承调用者的优先级和参与者上下文.

但是,使用以下命令运行任务

Task.detached(priority: .background) {
    //
}

输出如下所示:

handler : <_NSMainThread: 0x600003a28780>{number = 1, name = main}
task : <NSThread: 0x600003a6fe80>{number = 8, name = (null)}
load : <NSThread: 0x600003a6fe80>{number = 8, name = (null)}
result : <NSThread: 0x600003a7d100>{number = 6, name = (null)}

handler在主队列上运行,然后taskload在其他队列上运行,然后有趣的是,在loadWeather()返回后,result再次在完全不同的队列上运行.

之后,回答您的问题"why do not I see a crash if I use .detach for Task and get rid of MainActor.run in my code?",大概是因为您的ContentView是一个值类型,因此是线程安全的.如果将@State属性移动到WeatherLoader中的@Published,则会收到后台线程警告.

Swift相关问答推荐

RealityKit如何获得两个相对大小相同的ModelEntity?

如何将CodingKeys作为数组而不是枚举传递到类外部的函数中?

了解SWIFT中的命名VS位置函数调用

暂停我的 AR 会话并清除变量和锚点的功能

从 actor 的 init 方法调用方法

从 iPhone 中的安全飞地获取真正的随机数?

SwiftUI 从任务中安全更新状态变量

如何打印出此 struct 中的数据?

为 ObservedObject 和 State 对象配置预览

为什么 id 不能通过 struct 从 Objective-C 移植到 Swift?

SwiftUI View .tint(_ Color) 方法不起作用

使用 Swiftui 水平修剪 Shape()

用于 Swift 编码的 iOS 应用程序的 Ffmpeg

判断 Swift 程序是否正在输出到终端

在 Swift 中计算两个日期之间的差异

在 Swift 中获取双精度的小数部分

为什么 swift 中的类没有存储类型属性?

如何在 GCD 中停止 DispatchWorkItem?

哪些版本的 Xcode 支持哪些版本的 Swift?

Swift performSegueWithIdentifier 不起作用