Detecting When an Animation Has Ended
Tags: swift, swiftui, animation, ios
Published: 2020-07-30 19:43:19

Whenever a SwiftUI animation is triggered, its state is updated immediately regardless of the duration. The Animation struct does not provide us with any sort of callback to indicate whether it has completed. So how can we detect when our views have stopped animating?

![Moonshot](https://swiftui.diegolavalle.com/posts/animation-ended/screenshot.gif)

Suppose we want to animate the image of a little plane flying towards the moon. We start by declaring a view for the plane, the moon background and a state variable indicating the current distance from the ground. We lay all of this out on our main view's body.

```swift
struct PlaneMoonScene: View {
  
  @State var distance: CGFloat = 0

  var plane: some View {
    Image(systemName: "paperplane")
    …
  }

  var moonBackground: some View {
    Image(systemName: "moon.stars")
    …
  }

  var body: some View {
    ZStack(alignment: .bottomLeading) {
      moonBackground
      plane.offset(x: distance, y: -distance)
    }
  }
}
```

To kick off our flight we need to define a _launch_ button. The button's action will animate the distance from 0 to 200 by applying the `easeInOut()` timing function for a total duration of 1 second.

```swift
struct PlaneMoonScene: View {
  …
  var launchButton: some View {
    Button("Launch!") {
      withAnimation(.easeInOut(duration: 1)) {
        self.distance = 200
      }
    }
  }

  var body: some View {
    ZStack {
      …
      launchButton
    }
  }
}
```

As expected pressing _Launch!_ causes our little plane to shoot up and reach the moon. But what if we want to display a congratulatory message once our destination has been attained? For this we need to replace the `offset()` modifier that places the plane over the background with our own _FlyModifier_.

```swift
struct FlyModifier: AnimatableModifier {

  var totalDistance: CGFloat
  var percentage: CGFloat
  var onReachedDestination: () -> () = {}

  private var distance: CGFloat { percentage * totalDistance }

  …

  func body(content: Content) -> some View {
    content
    .offset(x: distance, y: -distance)
  }
}
```

Our `FlyModifier` also positions the plane at a specified distance but because it inherits from `AnimatableModifier` it has the ability to look at the animation parameters as they transition from initial value to target. Our modifier is applied similarly to `offset()` except this time around we _do_  get a chance to provide a completion handler.

```swift
struct PlaneMoonScene: View {

  @State var percentage: CGFloat = 0
  …

  var body: some View {
    …
    plane.modifier(
      FlyModifier(totalDistance: 200, percentage: percentage) {
        // We have reached the moon!
      }
    )
  }
}
```

So how do we actually detect within `FlyModifier` that the total distance has been covered - or that the percentage value is 1, as in 100 percent of the distance? Well we need to implement the variable `animatableData` to comply with `AnimatableModifier` and in the setter of that variable we check for completion. If the animation has finished we asynchronously invoke the handler given to us by the main view. 

```swift
struct FlyModifier: AnimatableModifier {
  …

  var animatableData: CGFloat {
    get { percentage }
    set {
      percentage = newValue
      checkIfFinished()
    }
  }

  func checkIfFinished() -> () {
    if percentage == 1 {
      DispatchQueue.main.async {
        self.onReachedDestination()
      }
    }
  }

  …
}
```

Finally we can celebrate reaching the moon!

```swift
struct PlaneMoonScene: View {

  @State var reachedMoon = false
  …

  var congrats: some View {
    Text("Congrats!!")
    …
  }

  var launchButton: some View {
    Button("Launch!") {
      withAnimation(.easeInOut(duration: 1)) {
        self.percentage = 1
      }
    }
  }

  var body: some View {
    ZStack {
      moonBackground
      plane.modifier(
        FlyModifier(totalDistance: 200, percentage: percentage) {
          withAnimation { self.reachedMoon.toggle() }
        }
      }
      launchButton
      if reachedMoon {
        congrats
      }
    }
  }
}
```

For sample code featuring this and other techniques please checkout our [working examples](https://github.com/swift-you-and-i/working-examples/tree/master/Sources/WorkingExamples/animation-ended) repo. Featured Example: [Moonshot](https://swiftui.diegolavalle.com/examples/moonshot/).

---

Originally published at [Swift You and I](https://swiftui.diegolavalle.com/posts/animation-ended/)