Reputation: 33
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()
}
}
Upvotes: 1
Views: 1159
Reputation: 5084
You have used the wrong thread, you should update the UI only on the main thread:
DispatchQueue.main.async { //main thread not global.
Please note: You should move those functions to your View
struct or to a shared class, as using global functions does not align with best practices.
Upvotes: 1