ProgressView updating on multiple steps of computation

Hello, I'm coding an app targeted at iOS but would be nice if it would still work (as is the case now) with iPadOS and macOS. This is a scientific app that performs matrices computations using LAPACK. A typical "step" of computation takes 1 to 5 seconds and I want the user to be able to run multiple computations with different parameters by clicking a single time on a button. For example, with an electric motor, the user clicks the button and it virtually performs the action of turning the motor, time step after time step until it reaches a final value. With each step, the “single step” computation returns a value, and the “multiple step” general computation gathers all these single step values in a list, the aim being to make a graph of these results.

So, my issue is making a progress bar so that the user knows where he is with the computation he issued. Using a for loop, I can run the multistep but it won’t refresh the ProgressView until the multistep computation is over. Thus, I decided to try DispatchQueue.global().async{}, in which I update the progress of my computation. It seems to work on the fact of refreshing the ProgressView but I get warnings that tell me I’m doing it the wrong way:

[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

I do not know how to publish on ProgressView, also because all the examples I come across on the Internet show how to update ProgressView with a timer, and that is not what I want to do, I want each computation to be achieved, send ProgressView the fact that it can update, and continue with my computation.

Also, what I did does not update correctly the final values. As the computation is asynchronous, the multistep function finishes instantaneously, showing a value of 0.0 while when the tasks finishes, the final value should be 187500037500.0. I show you an example code with these values, it’s a dummy and I put a huge for loop to slow down the code and ressemble a computation so you can see the update of the ProgressView.


import SwiftUI







class Params: ObservableObject {

    /// This is a class where I store all my user parameters

    @Published var progress = 0.0

    @Published var value = 0.0

}









struct ContentView: View {

    @StateObject var params = Params()

    @FocusState private var isFocused: Bool

    var body: some View {

        NavigationView {

            List {

                Section("Electromagnetics") {

                    NavigationLink {

                        Form {

                            ViewMAG_MultiStep(isFocused: _isFocused)

                        }

                        .toolbar {

                            ToolbarItemGroup(placement: .keyboard) {

                                Spacer()

                                Button("Done") {

                                    isFocused = false

                                }

                            }

                        }

                    } label: {

                        Text("Multi-step")

                    }

                }

            }

            .navigationTitle("FErez")

        }

        .environmentObject(params)

    }

}



struct ViewMAG_MultiStep: View {

    @FocusState var isFocused: Bool

    @EnvironmentObject var p: Params

    @State private var showResults = false

    @State private var induction = 0.0

    

    var body: some View{

        List{

            Button("Compute") {

                induction = calcMultiStep(p: p)

                showResults.toggle()

            }

            .sheet(isPresented: $showResults) {

                Text("\(induction)")

                

                ProgressView("Progress...", value: p.progress, total: 100)

                

            }



        }

        .navigationTitle("Multi-step")

    }

}





func calcSingleStep(p: Params) -> Double {

    /// Long computation, can be 1 to 5 seconds.

    var induction = p.value

    for i in 0...5000000 {

        induction += Double(i) * 0.001

    }

    return induction

}



func calcMultiStep(p: Params) -> Double{

    /// Usually having around 20 steps, can be up to 400.

    var induction = 0.0

    DispatchQueue.global().async {

        for i in 0...5 {

            induction += Double(i) * calcSingleStep(p: p)

            p.progress += 10.0

        }

        print("Final value of induction: \(induction)")

    }

    return induction

}





struct ContentView_Previews: PreviewProvider {

    static var previews: some View {

        ContentView()

    }

}





Answered by ShimizuTaisei in 725210022

It seems to work on the fact of refreshing the ProgressView but I get warnings that tell me I’m doing it the wrong way:

UI must be changed from the main thread. When you change values with @Published , the UI is updated. You can use DispatchQueue.main.async to change values from main thread.

Like this:

DispatchQueue.main.async {

    p.progress += 10.0

}

Also, what I did does not update correctly the final values.

I suggest below ⬇️

  1. Add @Published var induction = 0.0 to Params class
  2. Change value of induction (from main thread)
  3. Show on UI

Like this:

struct ViewMAG_MultiStep: View{

    @FocusState var isFocused: Bool

    @EnvironmentObject var p: Params

    @State private var showResults = false

//    @State private var induction = 0.0

    var body: some View{

        List{

            Button("Compute") {

                calcMultiStep(p: p)

                showResults.toggle()

            }

            .sheet(isPresented: $showResults) {

//                Text("\(induction)")

                Text("\(p.induction)")

                ProgressView("Progress", value: p.progress, total: 100)

            }

        }

        .navigationTitle("Multi-step")

    }

}

class Params: ObservableObject {

    @Published var progress = 0.0

    @Published var value = 0.0

    

    @Published var induction = 0.0

}

func calcMultiStep(p: Params) {

    var induction = 0.0

    DispatchQueue.global().async {

        for i in 0...5 {

            induction += Double(i) * calcSingleStep(p: p)

            // from main thread

            DispatchQueue.main.async {

                p.progress += 10.0

            }

        }

        print("Final value of induction: \(induction)")

        // from main thread

        DispatchQueue.main.async {

            p.induction = induction

        }

    }

}

I am not a native English speaker, so I apologize if my English is incorrect.

Accepted Answer

It seems to work on the fact of refreshing the ProgressView but I get warnings that tell me I’m doing it the wrong way:

UI must be changed from the main thread. When you change values with @Published , the UI is updated. You can use DispatchQueue.main.async to change values from main thread.

Like this:

DispatchQueue.main.async {

    p.progress += 10.0

}

Also, what I did does not update correctly the final values.

I suggest below ⬇️

  1. Add @Published var induction = 0.0 to Params class
  2. Change value of induction (from main thread)
  3. Show on UI

Like this:

struct ViewMAG_MultiStep: View{

    @FocusState var isFocused: Bool

    @EnvironmentObject var p: Params

    @State private var showResults = false

//    @State private var induction = 0.0

    var body: some View{

        List{

            Button("Compute") {

                calcMultiStep(p: p)

                showResults.toggle()

            }

            .sheet(isPresented: $showResults) {

//                Text("\(induction)")

                Text("\(p.induction)")

                ProgressView("Progress", value: p.progress, total: 100)

            }

        }

        .navigationTitle("Multi-step")

    }

}

class Params: ObservableObject {

    @Published var progress = 0.0

    @Published var value = 0.0

    

    @Published var induction = 0.0

}

func calcMultiStep(p: Params) {

    var induction = 0.0

    DispatchQueue.global().async {

        for i in 0...5 {

            induction += Double(i) * calcSingleStep(p: p)

            // from main thread

            DispatchQueue.main.async {

                p.progress += 10.0

            }

        }

        print("Final value of induction: \(induction)")

        // from main thread

        DispatchQueue.main.async {

            p.induction = induction

        }

    }

}

I am not a native English speaker, so I apologize if my English is incorrect.

Hello Shimizu,

Thank you so much for solving my problem in such a short time! It does the job perfectly and does not add complexity.

Best wishes,

Guillaume

ProgressView updating on multiple steps of computation
 
 
Q