Search code examples
swiftuiview

SwiftUI View Doesn't Change When @Published Bool Changes In View Model


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.


Solution

  • 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