Search code examples
swiftswiftui

How can I animate changes to an @ObservedObject?


My view is determined by state stored in a ViewModel. Sometimes the view might call a function on its ViewModel, causing an asynchronous state change.

How can I animate the effect of that state change in the View?

Here's a contrived example, where the call to viewModel.change() will cause the view to change colour.

  • Expected behaviour: slow dissolve from blue to red.
  • Actual behaviour: immediate change from blue to red.
class ViewModel: ObservableObject {

    @Published var color: UIColor = .blue

    func change() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.color = .red
        }
    }
}

struct ContentView: View {

    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        Color(viewModel.color).onAppear {
            withAnimation(.easeInOut(duration: 1.0)) {
                self.viewModel.change()
            }
        }
    }
}

If I remove the ViewModel and store the state in the view itself, everything works as expected. That's not a great solution, however, because I want to encapsulate state in the ViewModel.

struct ContentView: View {

    @State var color: UIColor = .blue

    var body: some View {
        Color(color).onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                withAnimation(.easeInOut(duration: 1.0)) {
                    self.color = .red
                }
            }
        }
    }
}

Solution

  • It looks as though using withAnimation inside an async closure causes the color not to animate, but instead to change instantly.

    Either removing the wrapping asyncAfter, or removing the withAnimation call and adding an animation modifier in the body of your ContentView (as follows) should fix the issue:

    Color(viewModel.color).onAppear {
        self.viewModel.change()
    }.animation(.easeInOut(duration: 1))
    

    Tested locally (iOS 13.3, Xcode 11.3) and this also appears to dissolve/fade from blue to red as you intend.