Search code examples
animationswiftuiobservable

How to perform a SwiftUI animation when an @Observable property changes


I'm animating a square into a circle in the following snippet. My view observes the isCircle property to determine whether to draw a circle or square. The MyData#doSomethingExpensive method is meant to simulate a long running task which, upon finishing, triggers the UI to animate (in this case animating a square to a circle).

This actually works as intended, but I have a withAnimation call in my @Observable UI model. I would rather have withAnimation inside the view, if possible. I would also like to continue using the @Observable API instead of switching back to ObservableObject conformance. Is there a way to refactor this such that the view will respond to changes in isCircle with the same animation that I have here:

import SwiftUI

@Observable
final class MyData {
    var isCircle = false
    func doSomethingExpensive() {
        Task {
            try await Task.sleep(for: .milliseconds(Int.random(in: 300...800)))
            withAnimation(.smooth(duration: 1.5)) {
                self.isCircle.toggle()
            }
        }
    }
}

struct ContentView: View {
    let myData = MyData()
    var body: some View {
        VStack {
            if myData.isCircle {
                Circle().fill(.blue).frame(width: 200)
            } else {
                Rectangle().fill(.red).frame(width: 200, height: 200)
            }
            Button("Animate later") {
                myData.doSomethingExpensive()
            }
        }
    }
}

Solution

  • I found a reasonable approach that works for my case. It requires wrapping the if in a ZStack and applying the .animation(value:) modifier to that ZStack:

    @Observable
    final class MyData {
        var isCircle = false
        func doSomethingExpensive() {
            Task {
                try await Task.sleep(for: .milliseconds(Int.random(in: 300...800)))
                self.isCircle.toggle()
            }
        }
    }
    
    struct ContentView: View {
        let myData = MyData()
        var body: some View {
            VStack {
                ZStack {
                    if myData.isCircle {
                        Circle().fill(.blue).frame(width: 200)
                    } else {
                        Rectangle().fill(.red).frame(width: 200, height: 200)
                    }
                }.animation(.smooth(duration: 1.5), value: myData.isCircle)
    
                Button("Animate later") {
                    myData.doSomethingExpensive()
                }
            }
        }
    }