Search code examples
swiftuiswiftui-animation

SwiftUI animation not working using animation(_:value:)


In SwiftUI, I've managed to make a Button animate right when the view is first drawn to the screen, using the animation(_:) modifier, that was deprecated in macOS 12.

I've tried to replace this with the new animation(_:value:) modifier, but this time nothing happens: So this is not working:

struct ContentView: View {
    @State var isOn = false
    var body: some View {
        Button("Press me") {
            isOn.toggle()
        }
        .animation(.easeIn, value: isOn)
        .frame(width: 300, height: 400)
    }
}

But then this is working. Why?

struct ContentView: View {
    var body: some View {
        Button("Press me") {
        }
        .animation(.easeIn)
        .frame(width: 300, height: 400)
    }
}

The second example animates the button just as the view displays, while the first one does nothing


Solution

  • The difference between animation(_:) and animation(_:value:) is straightforward. The former is implicit, and the latter explicit. The implicit nature of animation(_:) meant that anytime ANYTHING changed, it would react. The other issue it had was trying to guess what you wanted to animate. As a result, this could be erratic and unexpected. There were some other issues, so Apple has simply deprecated it.

    animation(_:value:) is an explicit animation. It will only trigger when the value you give it changes. This means you can't just stick it on a view and expect the view to animate when it appears. You need to change the value in an .onAppear() or use some value that naturally changes when a view appears to trigger the animation. You also need to have some modifier specifically react to the changed value.

    struct ContentView: View {
        @State var isOn = false
        //The better route is to have a separate variable to control the animations
        // This prevents unpleasant side-effects.
        @State private var animate = false
        
        var body: some View {
            VStack {
                Text("I don't change.")
                    .padding()
                Button("Press me, I do change") {
                    isOn.toggle()
                    animate = false
                    // Because .opacity is animated, we need to switch it
                    // back so the button shows.
                    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                        animate = true
                    }
                }
                // In this case I chose to animate .opacity
                .opacity(animate ? 1 : 0)
                .animation(.easeIn, value: animate)
                .frame(width: 300, height: 400)
                // If you want the button to animate when the view appears, you need to change the value
                .onAppear { animate = true }
            }
        }
    }