Chapters

Hide chapters

iOS Animations by Tutorials

Seventh Edition · iOS 15 · Swift 5.5 · Xcode 13

Section IV: Layer Animations

Section 4: 9 chapters
Show chapters Hide chapters

23. Intermediate Animations With UIViewPropertyAnimator
Written by Marin Todorov

You’ve already tried some animations with UIViewPropertyAnimator, and have started improving the Widgets project user experience by adding delightful animations to the interface. You’ve also looked into creating basic and keyframe animations and saw that using the UIViewPropertyAnimator class isn’t difficult at all! More importantly you’ve tackled some issues that aren’t as straightforward when using the UIView.animate(withDuration:...) set of APIs — for example, checking if an animation is currently running, conditionally adding animations and completions, and abstracting animations into standalone classes.

If you successfully completed the challenges from the previous chapter, just re-open the project and keep working on it. Otherwise, you can use the starter project provided for this chapter:

Let’s see how you can give your animations this extra something by using custom timings!

Custom Animation Timing

Throughout this book, you’ve been using the four built-in curves: linear, ease in, ease out, and ease in out. By now there isn’t much left to say about those that hasn’t been said already, so through most of this chapter you are going to focus on custom curves. If by any chance, you skipped over the earlier chapters where built-in curves are explained, have a quick detour to Chapter 3, “Getting Started With View Animations” and search for the Animation Easing section.

Assuming you have a solid grasp of what the four built-in curves are, let’s have a look at using one in an animation.

Built-in Timing Curves

Currently, when you activate the search bar you fade in a blur view on top of the widgets. In this example, you are going to remove that fade animation and animate the blur effect itself.

Open LockScreenViewController.swift and add a new method to the class:

func blurAnimations(_ blurred: Bool) -> () -> Void {
  return {

  }
}

This is a method that will return a prepared animations block, which you can add to an animator later on. Depending on the parameter value, the animations in the block will either remove or create the blur effect in blurView.

First let’s add the code to create the animations. Insert in between the braces:

self.blurView.effect = blurred ? 
  UIBlurEffect(style: .dark) : nil
self.tableView.transform = blurred ? 
  CGAffineTransform(scaleX: 0.75, y: 0.75) : .identity
self.tableView.alpha = blurred ? 0.33 : 1.0

Setting the effect property on blurView to either nil or a blur effect will spawn an animation. Furthermore, you adjust the visibility and transform of the table view, that contains the widgets. Great! Now the completed method looks like this:

func blurAnimations(_ blurred: Bool) -> () -> Void {
  return {
    self.blurView.effect = 
      blurred ? UIBlurEffect(style: .dark) : nil
    self.tableView.transform = blurred ? 
      CGAffineTransform(scaleX: 0.75, y: 0.75) : .identity
    self.tableView.alpha = blurred ? 0.33 : 1.0
  }
}

You can use this to produce two different animations depending on the current state of the screen.

Next you will need to adjust the UI. Scroll to viewDidLoad() and remove these two lines:

blurView.effect = UIBlurEffect(style: .dark)
blurView.alpha = 0

These are the lines that set the effect up, but you don’t need them anymore since you are going to use the new effect-based animations.

Speaking of which, replace the contents of toggleBlur(_:) with:

func toggleBlur(_ blurred: Bool) {
  UIViewPropertyAnimator(
    duration: 0.55, 
    curve: .easeOut, 
    animations: blurAnimations(blurred))
    .startAnimation()
}

Note that the curve parameter is of type UIViewAnimationCurve. This enumeration includes the four built-in curve types: .linear, .easeIn, .easeOut, and easeInOut.

Give the new animation a try; tap into the search bar and you will see the blur effect gradually appear on screen:

Notice how the blur doesn’t simply fade in or out, but it actually interpolates the amount of blur in the effect view.

Now you can try different timing curves like .easeIn or .linear to see how they alter the animation.

Note: Blur animations can be a bit jerky in the Simulator, to enjoy the power of iOS animations always enjoy the final effect on a device.

Custom Bézier Curves

Sometimes when you would like to be very specific about the timing of your animations, using these curves to simply “slow down at start” or “slow down about the end” isn’t enough.

Earlier in the book you learned that you can create a custom CAMediaTimingFunction and give your layer animations custom timings. However, this was mentioned briefly in a note and you didn’t have the chance to look more into that.

In this section, you are going to learn what Bézier curves are and how to use them to design your own custom animation timings. The good news is since UIViewPropertyAnimator uses layer animations behind the scenes, you can go back and apply what you master in this chapter to your layer animations as well.

But first — what are Bézier curves?

Note: If you already have a solid understanding of Bézier curves, skip over this rather simple explanation of the mechanics behind them.

Let’s start with something simple – a line. It’s pretty neat, because all you need to draw a line on screen are the coordinates of the two points that define it; the start (A) and the end (B):

The handy aspect of being able to describe a shape on screen so precisely is that you can also apply transforms to it: you can scale the line up, you can move it, and rotate it too. All thanks to those two points in a coordinate system. Further, you can persist lines to disk and load them back, because you can describe them with numbers, and you know how to persist those!

Now let’s look at curves. Curves are much more interesting than lines, because they can draw anything on screen. For example:

What you see above are four curves put together; their ends meet at the points where you see the little white squares. The interesting thing to note in the picture are the little green circles. After you look at the picture for a while you will notice that the circles “kind of define” where each one of the curves bends.

So curves are not random. They also have some specifics just like lines, which can help you define them via coordinates. You can then get all the benefits of coordinates like persisting them, transforming them, etc.

You define a curve by adding control points to lines. Let’s add one control point to the line we had before:

You can imagine the curve being drawn by a pencil attached to a line, whose start point moves along the line AC, and its end point moves along the line CB:

Bézier curves with one control point are called quadratic. You are, however, more interested in cubic Bézier curves — those have two control points.

You can also use cubic curves to describe animation timing. In fact, the built-in curves you’ve been using are also cubic curves that have been predefined for you.

Core Animation uses cubic curves that always start at coordinate (0, 0), which represents the beginning of the animation duration. Naturally the end point of these timing curves is always (1, 1) — the end of the duration and progress of the animation.

Let’s have a look at an ease-in curve:

As the time passes (moving left-to-right horizontally across the coordinate space), the curve makes very little progress on the vertical axis, Then about half way through the animation duration, progress speeds up and catches up with time so they both reach (1, 1) by the end of the animation.

It all makes sense now, right?

Can you guess which one below is an ease-out and which one is an ease-in-out curve?

Now that you understand how Bézier curves work, the only remaining question is how to visually design some curves and get the control points’ coordinates so you can use them for an iOS animation.

Note: Open a web browser and visit http://cubic-bezier.com. This is a handy web site by computer science researcher and speaker Lea Verou. It allows you to drag around the two control points of a cubic Bézier and see an instant animation preview.

I encourage you to play around until you have a good idea how different timing curves affect animation timing.

Next, you’ll move on to adding a custom timing animation to the Widgets project.

Open LockScreenViewController.swift and replace the existing animation in toggleBlur() with:

func toggleBlur(_ blurred: Bool) {
  UIViewPropertyAnimator(duration: 0.55,
    controlPoint1: CGPoint(x: 0.57, y: -0.4),
    controlPoint2: CGPoint(x: 0.96, y: 0.87),
    animations: blurAnimations(blurred))
    .startAnimation()
}

This is the second convenience initializer of the UIViewPropertyAnimator class. Besides the duration and animations block, it takes two control points as parameters. These help you define your custom cubic curve.

Wait a second! One of those points above has a negative coordinate! Indeed! Since you’re anyways doing a custom timing curve, why not do something exotic?

You can drag the control points so they pull the curve into the space defining negative values for the progress axis. The effect is rather amusing: If you’re moving a view in the right direction from point A to point B, it’ll first take a “step back” leftwards, and then continue in the correct direction towards point B.

In the case of your blur and scale animations, the table view will actually first scale up a bit before scaling down. Doing this adds a bit of “elasticity” to your animation. Be careful though: If you overdo it, this will create a rather comical effect on your animation.

Spring Animations

There is another convenience initializer — UIViewPropertyAnimator(duration:dampingRatio:animations:) — for defining spring driven animations.

This will produce the same animation as UIView’s animate(withDuration: delay: usingSpringWithDamping: initialSpringVelocity: options: animations: completion:) with an initial velocity of 0.

Like the UIView method, this API creates spring animations backwards as discussed in Chapter 13. You provide the duration you’d like to have for your animation, and UIKit calculates all aspects of the spring that would give you that duration. You know that doesn’t give as good a spring effect as doing the calculation properly. Luckily, there is a better way of creating spring animations with UIViewPropertyAnimator.

Custom Timing Providers

Meet the fourth and last initializer you’re going to cover here: UIViewPropertyAnimator(duration:timingParameters:).

This time, you can create a whole new object that could provide any timing data for your animations! You can use one of the UIKit objects that let you define custom cubic or spring based timings, but you can also roll out your own.

You’ll see how to create a custom spring animation before moving on to the next section in this chapter where you’ll create some spring animations in practice.

The second parameter named timingParameters is of type UITimingCurveProvider — a protocol defined by UIKit. There two classes in UIKit that conform to that protocol: UICubicTimingParameters and UISpringTimingParameters.

Let’s look at UISpringTimingParameters.

Providing Damping and Velocity

Even if you’re using a custom timing provider, you can still chose to go the easy way and provide just the damping ratio and initial velocity as you do when using the convenience initializer. The code would look like this:

let spring = UISpringTimingParameters(
  dampingRatio: 0.5, 
  initialVelocity: CGVector(dx: 1.0, dy: 0.2))

let animator = UIViewPropertyAnimator(
  duration: 1.0, 
  timingParameters: spring)

The spring parameter represents the configuration of your spring, and you provide it to your animator object to use for the timing of your animations. This would still calculate the spring “backwards” as discussed earlier.

Note how initial velocity is a vector type. UIKit will apply a two-dimensional initial velocity at the start, in case you are animating the position or size of any of your views. If you’re animating alpha or a single axis of your view’s location, UIKit will consider only the dx property of your initial velocity vector.

initialVelocity is also an optional parameter so if you don’t need to set a velocity at all, simply provide a damping ratio.

Custom Springs

If you would like to be more specific about your spring, you can use a different initializer on UISpringTimingParameters that lets you specify the spring’s mass, stiffness, and damping, much like you did for your layer animations earlier in the book.

The code to configure a custom spring is thus:

let spring = UISpringTimingParameters(
  mass: 10.0, 
  stiffness: 5.0, 
  damping: 30, 
  initialVelocity: CGVector(dx: 1.0, dy: 0.2))

let animator = UIViewPropertyAnimator(
  duration: 1.0, 
  timingParameters: spring)

If you need a quick refresher on how all those parameters work, take a quick detour to Chapter 13 “Layer Springs”.

In the next section you will try some of those custom timing animations.

Auto Layout Animations

Phew! That was a rather lengthy theoretical part of the chapter, so I’m sure you’re excited to write some code and give few animations a try.

You became proficient in Auto Layout animations in Chapter 9, “Animating Constraints”, so it won’t come as a surprise to you that in the next part you are going to be animating some constraints.

Layout constraint animations with UIViewPropertyAnimator are very similar to how you create them with UIView.animate(withDuration:...). The trick was to update a constraint, and then call layoutIfNeeded() from within an animations block.

Let’s try the same with UIViewPropertyAnimator.

Open AnimatorFactory.swift and add a new factory method:

@discardableResult
static func animateConstraint(
  view: UIView, 
  constraint: NSLayoutConstraint, 
  by amount: CGFloat
) -> UIViewPropertyAnimator {

}

In this method, you are going to animate a change to a constraint’s constant and then call layoutIfNeeded() on the provided view.

What is this animation going to look like? No idea! It really depends what kind of constraint you provide to the method. If you give it a trailing space constraint, it will move the view horizontally; if you provide it with a height constraint, the view will scale up or down.

Inside the method create an animator:

let spring = UISpringTimingParameters(dampingRatio: 0.2)
let animator = UIViewPropertyAnimator(
  duration: 2.0, 
  timingParameters: spring)

animator.addAnimations {
  constraint.constant += amount
  view.layoutIfNeeded()
} 
return animator

You use the simple convenience spring initializer, then you simply change the constraint and trigger an Auto Layout pass.

Now switch to LockScreenViewController.swift and add to viewWillAppear(_:):

dateTopConstraint.constant -= 100
view.layoutIfNeeded()

This will move the date label up by 100 points like so:

Next, trigger an animation to move the label (and all other views attached to it) down to its original location.

Append the following to viewDidAppear(_:):

AnimatorFactory.animateConstraint(
  view: view, 
  constraint: dateTopConstraint, 
  by: 100)
  .startAnimation()

Run the app and check out the resulting animation! Since you are moving down all views and scaling up your table view, this results in a rather complex yet smooth transition:

The animation looks a bit excessive though. The first couple of times, it looks nice, but if your users see this bouncy transition multiple times every day they are certainly going to hate your app.

This is a nice reminder that UI spring animations should be all about moderation.

Switch back to AnimatorFactory.swift and change the parameters of your spring animation. Set dampingRatio to 0.55, and duration to 1.0. This should make the animation more subtle yet still playful.

Next, you’ll explore a different situation when you will animate constraints. Currently, when you tap on Show More the widget changes its Height constraint and reloads the top table view contents to resize the widget.

In the next animation, you are going to animate the cell height change.

Open the WidgetCell.swift file and find toggleShowMore(_:). You can peek inside to see the current code that changes the cell height and reloads the parent table view.

You will completely rebuild this method. Remove all the code within toggleShowMore(_:) and replace it with:

self.showsMore.toggle()

First let’s define the animations you’d like to run. Append to toggleShowMore(_:):

let animations = {
  self.widgetHeight.constant = self.showsMore ? 230 : 130
  if let tableView = self.tableView {
    tableView.beginUpdates()
    tableView.endUpdates()
    tableView.layoutIfNeeded()
  }
}

In this piece of code, you perform a little trick. First you change the constraint as usual, but then you call beginUpdates() and endUpdates() on the table view. Doing this will ask all cells about their height and adjust the layout as needed. If any of your cells says it wants to be higher or shorter, UIKit will adjust its frame accordingly.

At the end of the block, you call layoutIfNeeded() to ensure the layout change will happen inside the animations block.

Now let’s create the animator. Add to the end of toggleShowMore(_:):

let spring = UISpringTimingParameters(
  mass: 30, 
  stiffness: 1000, 
  damping: 300, 
  initialVelocity: CGVector(dx: 5, dy: 0))

toggleHeightAnimator = UIViewPropertyAnimator(duration: 0.0, timingParameters: spring)
toggleHeightAnimator?.addAnimations(animations)
toggleHeightAnimator?.startAnimation()

The view already features a property called toggleHeightAnimator, so you simply create a spring configuration and store the new animator in that property. Note that in this case you define all of the spring properties, so the duration that you pass to the property animator is ignored. The spring itself determines how long the animation takes to run.

Run the app, tap Show More, and enjoy a smooth spring-driven animation:

At the bottom of the method, add the following code that reloads the icons in the widget:

widgetView.expanded = showsMore
widgetView.reload()

This code triggers reloadData() on the collection view and that reloads all icons. Try the animation again and this time you see a different number of icons depending on the widget height:

Built-in View Transitions

To finish up this animation, you’ll have a look at using the built-in view transitions with UIViewPropertyAnimator. In Chapter 5, “Transitions”, you learned about the built-in view transitions you can use in iOS. Now you are going to use a cross-fade to change the title of the widget button from Show More to Show Less and vice versa.

Inside toggleShowMore(_:) add this code after you define your animations block (but before you define your animator):

let textTransition = {
  UIView.transition(
    with: sender, 
    duration: 0.25, 
    options: .transitionCrossDissolve,
    animations: {
      sender.setTitle(
        self.showsMore ? "Show Less" : "Show More", 
        for: .normal)
    },
    completion: nil)
}

You define a view transition, and inside its animations block, you alter the button title depending whether the widget is currently expanded or not. So how do you add this transition to your animator? Just as you would with any other animations block!

Find the spot where you add animations to your animator and add textTransition as well with a 0.5 delay factor. The final code should look like this:

toggleHeightAnimator?.addAnimations(animations)
toggleHeightAnimator?.addAnimations(textTransition, delayFactor: 0.5)
toggleHeightAnimator?.startAnimation()

This will change the button title with a nice cross-fade effect:

In case you’d like to try other transitions go ahead and replace .transitionCrossDissolve with .transitionFlipFromTop - this is my personal favorite. This transition makes updating the button title look extra fancy.

You’re starting to push UIViewPropertyAnimator to its limits! Before you move on to the next chapter and interactive animations, make sure to look into this chapter’s challenge, which will introduce you to creative additive animations with UIViewPropertyAnimator.

Key Points

  • With UIViewPropertyAnimator, you can animate UIBlurEffect values and create impressive blur animations.

  • You can create your own custom animation timings and move away from the predefined easing constants by using two control point custom Bézier curves with UIViewPropertyAnimator.

  • Last but not least, you can mix in the pre-defined UIKit transition APIs like UIView.transition(with:duration:options:animations:completion) with other UIViewPropertyAnimator animations and they’ll “just work” together.

Challenges

Challenge 1: Additive Animations

When using UIView.animate(withDuration:...), adding animations to the same view property happens additively.

For example, if you are moving a view across the screen from point A to B, and change your mind mid-way about the end point and decide to send the view over to point C instead, it will not just break off the movement at the current point and move directly towards the new end point.

UIKit is smarter than that, so by default it will try to “ease” your view into its new trajectory. The actual movement will look something like this:

The animations aren’t replaced at the time you add changes, but are combined so that the changes happen additively.

UIViewPropertyAnimator can handle these kinds of situations as well (with varying success for different timing providers).

For this challenge, if the menu state is switched again in toggleShowMore(_:) before the previous animation completes, then instead of creating a new animator you should “add” animations to the existing toggleHeightAnimator.

  • Check if toggleHeightAnimator is currently running using isRunning.
  • isRunning can be a little… unreliable, so also check that fractionComplete is less than 1.
  • If the animator has already been created, pause it, add the new animation block, and continue the animation!
  • I did mention that some timing parameters work better with this than others - try adjusting the initial spring velocity to zero or replacing the spring altogether if it isn’t looking right.

When you’re done with this challenge, move on to the next chapter to learn how to add interactivity to your property animator animations.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.