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()
}
}
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 ⬇️
- Add
@Published var induction = 0.0
toParams
class - Change value of
induction
(from main thread) - 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.