I am creating an app that has some code that takes a bit to execute. I want it to hang the app as I don't want the user to make any changes when it's executing. It's a Multiplatform app so when executing in macOS it automatically changes the mouse to a rolling ball image so that works-ish. In iOS there's no feedback at all. I want to create a popup (thinking alert but not too fussy) that displays an updating message showing the user what's happening and making it obvious they have to wait.
Right now my View calls a class that executes the code so I wanted to somehow pass a variable that gets updated in the class but is visible in the View in real-time. Ideally I would want to be able to use this and call different methods each time from other Views but still use a popup with messages updating the user while the code executes.
To simplify this I created a mini project but I can't get it to work on either the macOS OR iOS as the View (app) isn't updated until after the code finishes executing (also have print statements to know what's happening). I've been trying @StateObject, @Published, ObservableObject, etc to no avail.
Code: ContentView.swift
import SwiftUI
import CoreData
struct ContentView: View {
@StateObject var progress = myProgress()
@State var showProgress:Bool = false
var body: some View {
NavigationView {
VStack {
Button(action: {
print("Pressed Button 1")
showProgress = true
}, label: {
Text("Execute Option")
})
.sheet(isPresented: $showProgress, content: {
Text(progress.message)
.onAppear() {
Task {
let longMethod = longMethodsCalled(currProgress: progress)
print("About to call method - \(progress.message)")
let error = longMethod.exampleOne(title: "My Passed In Title")
// Here I can use the returned value if it's an object or now if it passed if it's String?
print("Error: \(error ?? "No error")")
print("after printing error - \(progress.message)")
}
// If this is commented out it just shows the last message after code finishes
showProgress = false
}
})
}
}
}
}
Other file: longMethodsCalled.swift
import Foundation
import SwiftUI
class myProgress: ObservableObject {
@Published var message = "progressing..."
func changeMessage(newMessage:String) {
message = newMessage
print("Message changing. Now: \(message)")
self.objectWillChange.send()
}
}
class longMethodsCalled {
@State var progress: myProgress
init(currProgress:myProgress) {
progress = currProgress
}
public func exampleOne(title:String) -> String? {
print("in example one - \(title)")
progress.changeMessage(newMessage: "Starting example one")
sleep(1)
print("after first sleep")
progress.changeMessage(newMessage: "Middle of example one")
sleep(1)
progress.changeMessage(newMessage: "About to return - example one")
return "result of example one"
}
}
I guess I'm wondering if this is even possible? And if so how can I go about it. I can't tell if I'm close or completely out to lunch. I would REALLY love a way to update my users when my code executes.
Thanks for any and all help.
CodePudding user response:
Here an example using binding to do all view update in another struct. It is using async and await. For the sleep(), it use Task.sleep which does not lock queues.
struct LongMethodCallMessage: View {
@State var showProgress:Bool = false
@State var progressViewMessage: String = "will do something"
var body: some View {
NavigationView {
VStack {
Button(action: {
print("Pressed Button 1")
progressViewMessage = "Pressed Button 1"
showProgress = true
}, label: {
// text will be return value
// so one can see that it ran
Text(progressViewMessage)
})
.sheet(isPresented: $showProgress, content: {
// create the vue that will display the progress
TheLongTaskView(progressViewMessage: $progressViewMessage, showProgress: $showProgress)
})
}
}
}
}
struct TheLongTaskView: View, LongMethodsCalledMessage {
@Binding var progressViewMessage: String
@Binding var showProgress: Bool
var body: some View {
Text(progressViewMessage)
.onAppear() {
// create the task setting this as delegate
// to receive message update
Task {
let longMethod = LongMethodsCalled(delegate: self)
print("About to call method - \(progressViewMessage)")
let error = await longMethod.exampleOne(title: "My Passed In Title")
// Here I can use the returned value if it's an object or now if it passed if it's String?
print("Error: \(error ?? "No error")")
print("after printing error - \(progressViewMessage)")
// save the error message and close view
progressViewMessage = error!
showProgress = false
}
}
}
// updating the text
func changeMessage(newMessage:String) {
print("changeMessage: \(newMessage)")
progressViewMessage = newMessage
}
}
// a protocol to update the text in the long running task
protocol LongMethodsCalledMessage {
func changeMessage(newMessage:String)
}
class LongMethodsCalled {
var delegate: LongMethodsCalledMessage
init(delegate: LongMethodsCalledMessage) {
self.delegate = delegate
}
// make the method async
public func exampleOne(title:String) async -> String? {
print("in example one - \(title)")
self.delegate.changeMessage(newMessage: "Starting example one")
// this wait enable the text to update
// the sleep() may lock and prevent main queue to run
try! await Task.sleep(nanoseconds: 2_000_000_000)
print("after first sleep")
self.delegate.changeMessage(newMessage: "Middle of example one")
try! await Task.sleep(nanoseconds: 2_000_000_000)
print("after second sleep")
self.delegate.changeMessage(newMessage: "About to return - example one")
return "result of example one"
}
}