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?
>Solution :
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()
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)}
As you can see, handler
and task
run on the main queue, but load
runs on some other. As the code within the Task itself is guaranteed to run on the main queue, it’s safe to update State variables from there