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

问题描述 投票:0回答:3

这是我的观点:

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
变量的正确方法是什么?我发现如果我摆脱
MainActor.run
就不会失败,但是在处理
UIKit
时,我们必须从主线程调用此更新。这里有什么不同吗?我还了解到 Task 继承了
MainActor
上下文,因此我放置了
Task.detached
以确保它是除 main 之外的另一个线程。谁能帮我解释一下吗?

swift swiftui async-await
3个回答
5
投票

如果您使用

运行任务
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}

如您所见,

handler
task
在主队列上运行,
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
在主队列上运行,然后
task
load
在其他队列上运行,然后有趣的是,在
result
返回后,
loadWeather()
再次在完全不同的队列上运行。

毕竟,回答你的问题“如果我使用 .detach 作为任务并在我的代码中摆脱 MainActor.run ,为什么我没有看到崩溃?”,大概这是因为你的

ContentView
是一个值类型,因此线程安全。如果您将
@State
属性移至
@Published
中的
WeatherLoader
,那么您将收到后台线程警告。


2
投票

在 SwiftUI 中,我们使用来自

.task
的 async/await,而不是来自
Button
。我们还可以更新其中的
@State
变量。试试这个:

@Environment(\.weatherLoader) var weatherLoader
@State var loading = false
@State var result: Result<Item>? = nil
...

Button(loading ? "Cancel" : "Load") {
    loading.toggle()
}
.task(id: loading) {
    if !loading {
        return 
    }
    result = await weatherLoader.loadWeather()
    loading = false
}

.task
将在
loading
的值发生变化时运行,并在视图消失时取消。如果您想取消任务并在每次按下按钮时重新启动它,您可以将
refresh
从布尔值更改为
refreshCounter
并在按下按钮时递增它。

希望

weatherLoader
是一个结构体,因为
View
结构体不应该初始化对象,这是内存泄漏。


0
投票

你说:

我…了解到

Task
继承了
MainActor
上下文,所以我放置了
Task.detached
以确保它是另一个线程而不是
main

最终目标是确保您更新主要参与者(以及主线程)上的属性是正确的。这些必须由主角更新。

话虽如此,您不需要(也不想)使用分离任务。当主角点击

await
时,特定的执行路径就会暂停,主角可以自由地继续做其他事情,直到
loadWeather
完成。
await
不会阻塞当前线程,而是释放它去做其他事情。这消除了所有 GCD 的愚蠢行为:“让我将其分派到某个后台队列,完成后,将更新分派回主队列。”

所以,请考虑:

Button("Обновить") {
    refreshable = false
    Task.detached {
        let result = await weatherLoader.loadWeather()
        await MainActor.run {
            switch result {
            case .success(let item):
                temperature = item.temperature
                …
            case .failure:
                temperature = ""
                …
            }
            
            refreshable = true
        }
    }
}

这应该简化为:

Button("Обновить") {
    refreshable = false
    Task {
        let result = await weatherLoader.loadWeather()
        
        switch result {
        case .success(let item):
            temperature = item.temperature
            …
        case .failure:
            temperature = ""
            …
        }
        
        refreshable = true
    }
}

您需要使用分离任务的唯一时间是当您有一些缓慢的同步任务需要脱离当前参与者时。

但这不是这里发生的事情。您有一个异步

loadWeather
API,您
await
。因此,您可以使用
Task {…}
,它在当前参与者(即主要参与者)上运行任务。


所以,你问:

问题是 - 从

@State
上下文更新
async
变量的正确方法是什么?

正确的解决方案是将这些属性标记为

@MainActor
,然后如果您尝试从错误的上下文更新它们,编译器会警告您。

或者,我个人没有将各个属性标记为

@MainActor
,而是将所有这些逻辑从视图中拉出,并将其放在与主要参与者隔离的视图模型上。例如:

@MainActor
class ViewModel: ObservableObject {
    @Published var temperature = ""
    @Published var pressure = ""
    @Published var humidity = ""
    @Published var tickmark = ""
    @Published var refreshable = true

    private let weatherLoader = WeatherLoader()

    func update() async {
        refreshable = false

        let result = await weatherLoader.loadWeather()

        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
    }
}

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    var body: some View {
        GeometryReader { metrics in
            VStack(spacing: 0) {
                Grid(horizontalSpacing: 0, verticalSpacing: 0) {
                    GridRow {
                        Text("Температура")
                            .frame(width: metrics.size.width/2)
                        Text("\(viewModel.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("\(viewModel.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("\(viewModel.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("\(viewModel.tickmark)")
                            .frame(width: metrics.size.width/2)
                    }.frame(height: metrics.size.height*0.8*0.25)
                }.frame(height: metrics.size.height*0.8)

                Button("Обновить") {
                    Task { await viewModel.update() }
                }
                .disabled(!viewModel.refreshable)
                .padding()
            }
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

坦白说,与某些第三方Web服务的集成无论如何都不属于我们的考虑范围。

然后你问:

我发现如果我摆脱

MainActor.run
就不会失败,但是在处理 UIKit 时我们必须从主线程调用此更新。这里有什么不同吗?

不,这里也一样。它必须在主线程上更新。如果您使用主要参与者,那将确保您使用主线程。您不需要

MainActor.run
。 (十有八九,使用
MainActor.run
是一个错误,并且通过首先在正确的参与者上获取属性和方法可以更好地实现。)

© www.soinside.com 2019 - 2024. All rights reserved.