Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI withAnimation inside conditional not working

I would like to have a view with an animation that is only visible conditionally. When I do this I get unpredictable behavior. In particular, in the following cases I would expect calling a forever repeating animation inside onAppear to always work regardless of where or when it initializes, but in reality it behaves erratically. How should I make sense of this behavior? How should I be animating a value inside a view that conditionally appears?

Case 1: When the example starts, there is no circle (as expected), when the button is clicked the circle then starts as animating (as expected), if clicked off then the label keeps animating (which it shouldn't as the animated value is behind a false if statement), if clicked back on again then the circle is stuck at full size and while the label keeps animating

struct TestButton: View {
  @State var radius = 50.0
  @State var running = false
  let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)
  
  var body: some View {
    VStack {
      Button(running ? "Stop" : "Start") {
        running.toggle()
      }
      if running {
        Circle()
          .fill(.blue)
          .frame(width: radius * 2, height: radius * 2)
          .onAppear {
            withAnimation(animation) {
              self.radius = 100
            }
          }
      }
    }
  }
}

Case 2: No animation shows up regardless of how many times you click the button.

struct TestButton: View {
  @State var radius = 50.0
  @State var running = false
  let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)
  
  var body: some View {
    VStack {
      Button(running ? "Stop" : "Start") {
        running.toggle()
      }
      if running {
        Circle()
          .fill(.blue.opacity(0.2))
          .frame(width: radius * 2, height: radius * 2)
      }
    }
    // `onAppear` moved from `Circle` to `VStack`.
    .onAppear {
      withAnimation(animation) {
        self.radius = 100
      }
    }
  }
}

Case 3: The animation runs just like after the first button click in Case 1.

struct TestButton: View {
  @State var radius = 50.0
  @State var running = true  // This now starts as `true`
  let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)
  
  var body: some View {
    VStack {
      Button(running ? "Stop" : "Start") {
        running.toggle()
      }
      if running {
        Circle()
          .fill(.blue.opacity(0.2))
          .frame(width: radius * 2, height: radius * 2)
      }
    }
    .onAppear {
      withAnimation(animation) {
        self.radius = 100
      }
    }
  }
}
like image 643
Ethan Kay Avatar asked Dec 17 '25 23:12

Ethan Kay


1 Answers

It is better to join animation with value which you want to animate, in your case it is radius, explicitly on container which holds animatable view.

Here is demo of approach. Tested with Xcode 13.2 / iOS 15.2

demo

struct TestButton: View {
    @State var radius = 50.0
    @State var running = false
    let animation = Animation.linear(duration: 1).repeatForever(autoreverses: false)

    var body: some View {
        VStack {
            Button(running ? "Stop" : "Start") {
                running.toggle()
            }
            VStack {           // responsible for animation of
                               // conditionally appeared/disappeared view
                if running {
                    Circle()
                        .fill(.blue)
                        .frame(width: radius * 2, height: radius * 2)
                        .onAppear {
                            self.radius = 100
                        }
                        .onDisappear {
                            self.radius = 50
                        }
                }
            }
            .animation(animation, value: radius)  // << here !!
        }
    }
}
like image 121
Asperi Avatar answered Dec 20 '25 15:12

Asperi