SwiftUI safe update state variables from task

Advertisements

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

Leave a Reply Cancel reply