Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 53

Pan-Driven Animations with `UIViewPropertyAnimator`

Recommended resources

Hello! Welcome back to our module on "Interactive Animations and Transitions."

In our previous lesson, we explored the theory behind UIViewPropertyAnimator, understanding its key benefits for creating interactive, interruptible, and reversible animations. We saw how the fractionComplete property is central to this interactivity and discussed the common pattern of using a gesture recognizer's began, changed, and ended states to control the animation lifecycle.

Today, we move from theory to practice. Our goal is to implement an interactive animation driven by a pan gesture recognizer using UIViewPropertyAnimator. You will learn how to structure the code, handle the gesture states, and add refinements that create a truly polished and professional-feeling user interaction.

The Foundation: Driving an Animator

Before we jump into pan gestures, let's solidify our understanding of how to drive an animator's progress. A UISlider, which provides a value from 0.0 to 1.0, is a perfect analog for the progress we'll later calculate from a pan gesture.

The following video provides a detailed, step-by-step walkthrough of creating an interactive animation controlled by a slider. It covers programmatic UI setup, creating the animator, and linking the slider's value to fractionComplete.

Advanced Animations: UIViewPropertyAnimator Blur Effect (Ep 1)

In this video, "Advanced Animations: UIViewPropertyAnimator Blur Effect" from Lets Build That App, we'll see a complete implementation of a scrubbable animation. Pay close attention to how the slider's value is used to drive the fractionComplete property.

Please watch from 05:41 to 25:21. The first part (until ~11:33) covers setting up the UI programmatically, which is a good review. The core of our interest is from ~20:25 onwards, where the UIViewPropertyAnimator is created and connected to the slider's handleSliderChange action. Notice how a single line, animator.fractionComplete = slider.value, creates the interactive effect.

The key takeaway from the video is the direct mapping: the slider's value (a Float between 0 and 1) is cast to a CGFloat and assigned directly to the animator's fractionComplete. As the slider moves, the animation scrubs backward and forward. Now, we'll achieve the same effect using a pan gesture.


Implementing the Pan Gesture Recognizer

A pan gesture provides a more direct and satisfying way for users to manipulate UI elements. The implementation pattern follows the state-based logic we discussed in our last lesson.

Advanced Animations with UIViewPropertyAnimator - AppCoda

The article "Advanced Animations with UIViewPropertyAnimator" by AppCoda provides an excellent text-based guide for implementing this pattern. We'll use it to structure our gesture handling code.

Please read the sections titled "Adding a pan gesture" and the initial part of its implementation. Focus on three things: How the UIPanagestureRecognizer is created and added to the view. The creation of the @objc func popupViewPanned(recognizer:) method. The switch statement that handles the .began, .changed, and .ended states of the gesture.

Let's break down the logic inside the pan handler function, as it's the heart of our implementation.

Handling Gesture States

Here's the pattern for managing the animator based on the gesture's state:

  1. .began: The user's finger touches down and starts moving.

    • Action: Create and configure your UIViewPropertyAnimator with all the desired animations (e.g., final frame, alpha, transform).
    • Crucial Step: Immediately call animator.pauseAnimation(). This moves the animator to the active state but freezes it at fractionComplete = 0.0. The animation is now ready and waiting for your control.
    // From the AppCoda article
    case .began:
        // 'toggle()' is their helper method to create and configure the animator
        toggle() 
        animator.pauseAnimation() 
    
  2. .changed: The user is dragging their finger across the screen.

    • Action: Get the gesture's translation using recognizer.translation(in: view).
    • Calculate what fraction of the total animation distance this translation represents. This often involves dividing the translation distance by a predefined maximum distance (popupOffset in the article).
    • Crucial Step: Update the animator's progress: animator.fractionComplete = calculatedFraction. This is what scrubs the animation.
    // From the AppCoda article
    case .changed:
        let translation = recognizer.translation(in: collectionView)
        var fraction = -translation.y / popupOffset // Calculate progress
        animator.fractionComplete = fraction // Drive the animation
    
  3. .ended: The user lifts their finger.

    • Action: Tell the animator to finish what it was doing.
    • Crucial Step: Call animator.continueAnimation(withTimingParameters: nil, durationFactor: 0). The animator will proceed from its current fractionComplete to its end state (either 1.0 or 0.0 if reversed), using its original timing curve and duration. The durationFactor of 0 means the remaining animation will use a duration proportional to the remaining fraction.
    // From the AppCoda article
    case .ended:
        animator.continueAnimation(withTimingParameters: nil, durationFactor: 0)
    

Polishing the Interaction

The basic implementation works, but to achieve a professional feel, we need to handle interruptions and reversals gracefully.

1. Handling Interruptions

Imagine a user taps a card to start an expansion animation. Before it finishes, they touch down again to "catch" it and drag it. With our current code, the animation would jump because the pan gesture's fractionComplete calculation starts from zero, ignoring the animation's existing progress.

To fix this, we store the animation's progress when the pan begins.

Advanced Animations with UIViewPropertyAnimator - AppCoda

Let's return to the AppCoda article to see how to make interruptions seamless.

Read the short section that starts with "Now that the animation looks pretty good, but you can’t interrupt the animation in the middle." This introduces the animationProgress property and shows how to use it.

The logic is simple but effective:

  • Declare a property to store the progress: private var animationProgress: CGFloat = 0.
  • In the .began case, capture the animator's current progress: animationProgress = animator.fractionComplete.
  • In the .changed case, add this stored progress to the newly calculated fraction: animator.fractionComplete = fraction + animationProgress.

Now, if the user "catches" an animation that is already 30% complete, the pan gesture will correctly start scrubbing from that 30% mark instead of jumping back to 0%.

2. Reversing the Animation on "Fling"

What if the user starts dragging a view open but then "flings" it closed? The animation should reverse and complete in the opposite direction. We can achieve this by checking the gesture's velocity when it ends.

Advanced Animations with UIViewPropertyAnimator - AppCoda

The final refinement is to make the animation reversible based on gesture velocity.

Read the section titled "Reversing the animation." This shows how to get the gesture's velocity and use it in the .ended case to decide whether to set animator.isReversed = true.

The implementation in the .ended case becomes more sophisticated:

  1. Get the velocity: let velocity = recognizer.velocity(in: self).
  2. Determine the primary direction of the fling (e.g., let shouldComplete = velocity.y > 0).
  3. Based on the direction of the fling and the animation's current state (e.g., expanded or collapsed), you may need to toggle the animator's isReversed property.
  4. Finally, call animator.continueAnimation(...). The animator will now proceed in the correct direction.

This logic makes the UI feel much more responsive and intuitive, as it correctly interprets the user's intent.


Conclusion

Today you've translated the theory of UIViewPropertyAnimator into a practical, powerful implementation. By combining a UIPanGestureRecognizer with the animator's stateful API, you can build the kind of fluid, direct-manipulation interfaces that define modern iOS apps.

Key Takeaways:

  • The core pattern for interactive animations involves using a UIPanGestureRecognizer and handling its .began, .changed, and .ended states.
  • .began: Create the animator and immediately call pauseAnimation().
  • .changed: Calculate a progress value (0.0 to 1.0) from the gesture's translation and set animator.fractionComplete.
  • .ended: Call continueAnimation(...) to let the animator finish.
  • For professional-grade interactions, handle interruptions by storing animationProgress and use gesture velocity to decide whether to reverse the animation.

Preview of the Next Lesson:

We've focused on animating standard view properties like frame and alpha. In the next lesson, we will explore a different kind of animation: animating the strokeEnd property of a CAShapeLayer to create a line-drawing effect. This will broaden your animation skills to include custom shapes and paths.