Home > front end >  SwiftUI withAnimation inside conditional not working
SwiftUI withAnimation inside conditional not working

Time:01-24

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
      }
    }
  }
}

CodePudding user response:

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 !!
        }
    }
}
  •  Tags:  
  • Related