Tutorial

Advanced Animations with UIViewPropertyAnimator


Animations are cool. They are an important part of iOS Human Interface Guidelines. Animations help you to draw user’s attention to important things or just add some fun to your app.

There are few ways to implement animations in iOS. Probably, the most popular one is UIView.animate(withDuration:animations:). You can animate view’s layer with CABasicAnimation. Also, UIKit lets you customize animation of view controller’s presentation with UIViewControllerTransitioningDelegate.

In this tutorial, I want to discuss with you another exciting way to animate your views – UIViewPropertyAnimator. This class gives you a lot more control than its predecessor UIView.animate. With it, you can build custom timing, interactive, and interruptible animations. Furthermore, you can change the animator on the fly.

Sound confusing? No worries. You’ll understand in a while.

Starting with UIViewPropertyAnimator

UIViewPropertyAnimator was introduced in iOS 10. It allows you to create animations in object-oriented way. Let’s have at a sample animation created using UIViewPropertyAnimator.

This is how you will do it with UIView.

And this is how to do it with UIViewPropertyAnimator:

If you want to test the animation, simply create a Playground project and run the code like below. Both code snippet will result the same animation.

You may think, that there is not a big difference. So, what is the point to introduce a new way for creating animations? UIViewPropertyAnimator becomes more useful, when you want to create interactive animations.

Interactive and interruptible animations

Do you remember the classic “Slide to Unlock” gesture? Or the “Swipe from bottom” gesture for opening Control Center? These are perfect examples of interactive and interruptible animations. You can start moving a view with your finger, then release it and the view will go back to its original position. Alternatively, you can catch the view during the animation and continue dragging it with your finger.

UIView animations, however, don’t provide an easy way to control the completion percentage of an animation. You can’t pause an animation in the middle of a cycle and continue to execute it after the interruption.

This is the power of UIViewPropertyAnimator. You’ll see how we can build a fully interactive, interruptible, scrubbable, and reversible animation in a few steps.

Preparing the Starter Project

First, please download the starter project to get started. Once unzip the archive, you will find the CityGuide application, which helps users plan their vacations. User can swipe through the list of cities and then open a detailed card with a detailed information of the city user likes more.

Let’s explore the project code a little bit before creating the animations.
Here is what you can find in the Xcode project:

  1. ViewController.swift: The main view controller of our application with a UICollectionView to display an array of City objects.
  2. CityCollectionViewCell.swift: The cell for displaying City. In fact, most of the changes will be applied to this class in this tutorial. You may notice that we have descriptionLabel and closeButton defined in the class. However, if you run the app, you will not see both objects. No worries. We will make them visible later. In the class, we also have collectionView and index properties. Similarly, we will use them later for animations.
  3. CityCollectionViewFlowLayout.swift: This class is responsible for fancy horizontal scroll. We will not touch it at all.
  4. City.swift: The main model of our application. Also, we have a factory method here, which we used in ViewController.
  5. Main.storyboard: You can find UI for ViewController and CityCollectionViewCell there.

Try to build it and execute the sample app. This is what you should see.

cityguideapp-iphone8

Implementing the Expand and Collapse Animations

After launching the app, it shows a list of cities. But the user can’t interact with the cells. Now we want to show information for each city when a user taps one of the cell. Take a look at the final deliver. This is what we want to build:

The animation looks good, right? But nothing fancy here, it’s just some basic UIViewPropertyAnimator logic. Let’s see how to implement this type of animation. Create the collectionView(_:didSelectItemAt) method by inserting the following code snippet to the end of the ViewController file:

Now we need implement the toggle method. Let’s switch over to CityCollectionViewCell.swift and implement the method.

First, add State enum to the top of the file, right before the class declaration of CityCollectionViewCell. This enum allows us to keep track of the state of the cell:

Next, let’s add a few properties for controlling animation to the CityCollectionViewCell class:

The initialFrame variable is used to store the frame of cell before animation. state is used to track if the cell is expanded or collapsed. And, the animator variable is used to drive and control the animation.

Now add the toggle method and invoke it from the close action method like this:

Here we added two more methods: expand() and collapse() in the code. Let’s continue to implement them. First, we start with the expansion d() method:

Wow, that’s a lot of code. Let me explain to you step by step:

  1. First, we check if collectionView and index are not nil. Otherwise, we will not be able to run animation.
  2. Next, we start to create the animation by calling animator.addAnimations.
  3. Next, we store the current frame, which is used to restore it on the collapse animation.
  4. We then set the alpha value of both descriptionLabel and closeButton to make the visible.
  5. Next, we remove the rounded corner and set a new frame for the cell. The cell will be shown in full-screen.
  6. Next, we move the neighbor cells.
  7. Lastly, we call the animator.addComplete() method to disable interaction of the collection view. This prevents users from scrolling it while the cell is expanding. We also change the current state of the cell. It’s important that we only change the cell’s state when the animation completes.

Now we are going to add collapse animation. In brief, we just restore the cell to its previous state:

Now, it’s time to build our application. Try to tap on the cell and you should see the animation. To close the view, tap on the cross icon at the upper right corner.

Adding a pan gesture

You may argue we can achieve the same result by using UIView.animate. What’s the point of using UIViewPropertyAnimator?

Okay, it’s time to make the animation interactive. We will add a UIPanGestureRecognizer and a new property named popupOffset to track how much we can pan the cell. Let’s declare these variables in the CityCollectionViewCell class:

Next, add the following method to register the pan recognizer:

Now, we need to add the popupViewPanned method to track the pan gesture. Insert the following code in CityCollectionViewCell:

We have three states here. In the beginning of the gesture, we initialize the animator with the toggle method and immediately pause it. While the user is dragging the cell, we update the animation by setting the fractionComplete property of animator. This is the core magic of the animator that allows us to control the animator. Lastly, when the user releases his/her finger, we call the continueAnimation method of the animator to continue the execution of the animation. The cell will then go to the target position.

If you run the app, you can drag the cell up to expand it. And then drag the expanded cell down to collapse it.

Now that the animation looks pretty good, but you can’t interrupt the animation in the middle. Therefore, to make the animation fully interactive we have to add one more feature – interruption. The user can initiate the expand/collapse animation as usual, but the animation should be paused immediately once the user taps on the cell during the animation cycle.

To achieve it, we have to store the progress of the animation and then take this value into account to calculate the completion percentage of the animation.

First, let’s declare a new property to CityCollectionViewCell:

Next, update the .began case of the popupViewPanned method with the following line of code to remember the progress:

For the .changed case, you will need to update the following line of code to correctly compute the completion percentage:

Now you’re ready to test the app. Run the project and see what you get. If you follow me correctly, the animation should look this:

Reversing the animation

You may find a drawback for the current implementation. When you drag the cell up a little bit and then revert it to the original position, the cell will still continue to expand when you release the finger. Let’s fix this issue to make the interactive animation even better.

Update the .end case of the popupViewPanned method like this:

Now we take the velocity of the pan gesture into account to determine if the animation should be reversed.

Lastly, insert one more line of code in the .changed case. Put it right the computation of animator.fractionComplete.

Let’s build app again. Now everything should work smoothly.

Fixing the pan gesture

One more thing! Well, we completed the implementation of the animation with UIViewPropertyAnimator. However, there is one nasty bug. Probably, you may have experienced it while testing the app. The problem is that we can’t scroll horizontally over the cells. Try to swipe left/right over the cells and you’ll experience the issue.

The root cause is due to UIPanGestureRecognizer we created. It also catches the swipe gesture that conflicts with the built-in gesture recogniser of the UICollectionView.

Though the user can still swipe the upper/lower part of the cells or the space between cell to scroll through the cities, I don’t like such bad user experience. Let’s fix it.

To resolve the conflicts, we need to implement a delegate method called gestureRecognizerShouldBegin(_:). This method controls whether the gesture recognizer should proceed with interpreting touches. If you return false in the method, the gesture recognizer will then ignore the touches. So what we’re going to do is to instruct our own pan recognizer to ignore horizontal swipes.

To do that, let’s set the delegate of our pan recognizer. Insert the following line of code in the initialization of panRecognizer (you can put the code right before return recognizer:

Next, implement the gestureRecognizerShouldBegin(_:) method like this:

We will start opening/closing pan gesture if its vertical velocity is greater than its horizontal counterpart.

Cool! Let’s test the app again. You should now be able to navigate through the city records by swiping left/right the cells.

Bonus: Custom timing functions

Before we end this tutorial, let’s talk something about custom time functions. Do you still remember the last time when a designer asked you to implement custom timing function for your animation?

Usually you have to change UIView.animation to CABasicAnimation or wrap it in CATransaction. With UIViewPropertyAnimator, you can easily implement custom timing function.

Replace the animator initialization with this custom timing function (try to draw your own cubic bezier curve) like this:

Alternatively, instead of using cubic timing parameters, you can also use spring timing like this:

Try to run the project again and see what you get.

Conclusion

With UIViewPropertyAnimator, you can improve static screens and enhance user experience with interactive animations.

I know you can’t wait to implement what you learned into your own project. If you apply the technique in your project, it would be great if you can let me know by leaving a comment below.

For reference, you can download the final project here.

Further References

iOS
How to Access Photo Library and Use Camera in SwiftUI
Tutorial
Advanced Unit Testing in Swift Using Swinject, Quick and Nimble
Tutorial
RESTful API Tutorial: How to Upload Files to a Server
Shares