Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

SwiftUI safe update state variables from task

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?

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

>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

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading