loading...
Swift You and I

Detecting When an Animation Has Ended

diegolavalle profile image Diego Lavalle Originally published at swiftui.diegolavalle.com ・3 min read

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

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.

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.

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.

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.

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.

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!

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 repo. Featured Example: Moonshot.


Originally published at Swift You and I

Posted on by:

diegolavalle profile

Diego Lavalle

@diegolavalle

Mad computer scientist. Builder of apps. Apple Frameworks, Web Standards, Swift, Swift UI, Advanced Javascript, React, React Native, Node.

Swift You and I

Swift You and I is a publication centered around SwiftUI, Combine and related Apple frameworks. Official app (https://swiftui.diegolavalle.com/app) with interactive examples available on the App Store.

Discussion

pic
Editor guide