Here is my view :
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)
}
}
The question is - what is the right way to update @State
variables from async context? I see that there is no failure if I get rid of MainActor.run
but when dealing with UIKit
we must call this update from main thread. Does it differ here? I also learned that Task inherits MainActor
context, so I put Task.detached
to make sure that it's another thread than main. Could anyone make it clear for me?
CodePudding user response:
If you run a task using
Task { @MainActor in
//
}
then the code within the Task itself will run on the main queue, but any async calls it makes can run on any queue.
Adding an implementation of WeatherLoader
as follows:
class WeatherLoader {
func loadWeather() async throws -> Item {
print("load : \(Thread.current)")
try await Task.sleep(nanoseconds: 1_000_000_000)
return Item()
}
}
and then calling like:
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
}
you'll see something like
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}
As you can see, handler
and task
run on the main queue, load
runs on some other, and then result
is back on the main queue.
As the code within the Task itself is guaranteed to run on the main queue, it's safe to update State variables from there.
As you mention above, @MainActor in
isn't actually required in this case, Task(priority:operation:)
inherits the priority and actor context of the caller.
However, running a task using
Task.detached(priority: .background) {
//
}
gives an output like:
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
runs on the main queue,then task
and load
run on some other, and then interestingly result
is on an entirely different one again after loadWeather()
returns.
After all that, in answer your question "why do not I see a crash if I use .detach for Task and get rid of MainActor.run in my code?", presumably this is because your ContentView
is a value type, and therefore thread safe. If you move your @State
properties to @Published
in WeatherLoader
, then you will get background thread warnings.