I am truly at a loss here. I use @Published vars and view models all the time and have never run into this issue.
I am using a simple ternary to change what is shown on the view so I can have a progress indicator when something is loading. The only way I have gotten this view to update and show the indicator is when I take the VM out of the equation and use an @State private var (which I do not want to do). Even the onChange modifier in the view that should simply print to console when the @Published bool in the VM changes is not seeing anything.
Here's a simple example:
struct TestView: View {
@StateObject var viewModel: ViewModel
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
VStack {
Button("Loading Test") {
viewModel.testLoading()
}
}
.onChange(of: viewModel.showProgress) { _ in
print("View Showing: \(viewModel.showProgress ? "True" : "False")")
}
.disabled(viewModel.showProgress)
.blur(radius: viewModel.showProgress ? 3 : 0)
VStack {
Text("Loading...")
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.scaleEffect(1.5)
}
.frame(width: geometry.size.width / 3,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.opacity(viewModel.showProgress ? 1 : 0)
}
}
}
}
extension TestView {
@MainActor
class ViewModel: ObservableObject {
@Published var showProgress: Bool = false
func testLoading() {
self.showProgress = true
print("Show Progress: \(self.showProgress ? "True" : "False")")
sleep(5)
self.showProgress = false
print("Show Progress: \(self.showProgress ? "True" : "False")")
}
}
}
The console output from the function in the VM is showing that it is changing the @Published var properly, but no changes to that variable are seen in the view, except for the disabled modifier. You cannot tap anything in the view when the @Published var is true, and as soon as it becomes false, whatever you tried to tap activates (I would also like to prevent this behavior, but that's another question).
With this simple test, you should be able to tap the Loading Test button text and the showProgress @Published var is changed to true for 5 seconds and then back to false. When it's true, the view should be blurred and the progress indicator should be shown.
This is because you are making the main thread sleep and then you put the showProgress
back to false so when testLoading
is done it will look like the published property was never changed at all.
One way to do the same in an async way is this
func testLoading() {
Task {
self.showProgress = true
print("Show Progress: \(self.showProgress ? "True" : "False")")
try await Task.sleep(nanoseconds: 3_000_000_000)
self.showProgress = false
print("Show Progress: \(self.showProgress ? "True" : "False")")
}
}
Which will show the progress view and generate the following output when the button is pressed
Show Progress: True
View Showing: True
Show Progress: False
View Showing: False