Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI view not animating when bound to @Published var if action isn't completed immediately

Tags:

swiftui

I have a SwiftUI view that is swapping out certain controls depending on state. I'm trying to use MVVM, so most/all of my logic has been pushed off to a view model. I have found that when doing a complex action that modifies a @Published var on the view model, the View will not animate.

Here's an example where a 1.0s timer in the view model simulates other work being done before changing the @Published var value:

struct ContentView: View {
    @State var showCircle = true
    @ObservedObject var viewModel = ViewModel()

    var body: some View {

        VStack {
            VStack {
                if showCircle {
                    Circle().frame(width: 100, height: 100)
                }

                Button(action: {
                    withAnimation {
                        self.showCircle.toggle()
                    }

                }) {
                    Text("With State Variable")
                }
            }

            VStack {
                if viewModel.showCircle {
                    Circle().frame(width: 100, height: 100)
                }
                Button(action: {
                    withAnimation {
                        self.viewModel.toggle()
                    }
                }) {
                    Text("With ViewModel Observation")
                }
            }
        }
    }


    class ViewModel: ObservableObject {
        @Published var showCircle = true

        public func toggle() {
            // Do some amount of work here. The Time is just to simulate work being done that may not complete immediately.
            Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] _ in
                self?.showCircle.toggle()
            }

        }
    }
like image 279
lepolt Avatar asked Mar 10 '20 11:03

lepolt


1 Answers

In the case of view model workflow your withAnimation does nothing, because not state is changed during this case (it is just a function call), only timer is scheduled, so you'd rather need it as

Button(action: {
    self.viewModel.toggle()  // removed from here
}) {
    Text("With ViewModel Observation")
}

...

Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { [weak self] _ in
    withAnimation { // << added here
        self?.showCircle.toggle()
    }
}

However I would rather recommend to rethink view design... like

VStack {
    if showCircle2 { // same declaration as showCircle
        Circle().frame(width: 100, height: 100)
    }
    Button(action: {
        self.viewModel.toggle()
    }) {
        Text("With ViewModel Observation")
    }
    .onReceive(viewModel.$showCircle) { value in
        withAnimation {
            self.showCircle2 = value
        }
    }
}

Tested with Xcode 11.2 / iOS 13.2

like image 77
Asperi Avatar answered Sep 22 '22 09:09

Asperi