这是我的观点:
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 之外的另一个线程。谁能帮我解释一下吗?
如果您使用
运行任务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
,那么您将收到后台线程警告。
在 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
结构体不应该初始化对象,这是内存泄漏。
你说:
我…了解到
继承了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服务的集成无论如何都不属于我们的考虑范围。
然后你问:
我发现如果我摆脱
就不会失败,但是在处理 UIKit 时我们必须从主线程调用此更新。这里有什么不同吗?MainActor.run
不,这里也一样。它必须在主线程上更新。如果您使用主要参与者,那将确保您使用主线程。您不需要
MainActor.run
。 (十有八九,使用MainActor.run
是一个错误,并且通过首先在正确的参与者上获取属性和方法可以更好地实现。)