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

22. Getting Started With UIViewPropertyAnimator
Written by Marin Todorov

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

UIViewPropertyAnimator was introduced in iOS 10 and addressed the need to be able to create easily interactive, interruptible, and/or reversible view animations.

Before iOS 10, the only option to create view-based animations was the UIView.animate(withDuration:...) set of APIs, which did not provide any means for developers to pause or stop already running animations. Further, to reverse, speed up, or slow an animation, developers had to use layer-based CAAnimation animations.

UIViewPropertyAnimator makes creating all of the above a bit easier since it’s a class that lets you keep hold of running animations, lets you adjust the currently running ones and provides you with detailed information about the current state of an animation.

UIViewPropertyAnimator is a big step away from the pre-iOS 10 “fire-and-forget” animations. That being said, UIView.animate(withDuration:...) APIs still do play a big role in creating iOS animations; these APIs are simple and easy to use, and often times you really just want to start a short fade-out or a simple move and you really don’t need to interrupt or reverse those. In these cases using UIView.animate(withDuration:...) is just fine.

Keep in mind that UIViewPropertyAnimator does not implement everything that UIView.animate(withDuration:...) has to offer, so sometimes you will still need to fall back on the old APIs.

Basic Animations

Open and run the starter project for this chapter. You should see a screen similar to the lock screen in iOS. The initial view controller displays a search bar, a single widget, and an edit button at the bottom:

Some of the app’s functionality that doesn’t have to do with animations is already implemented for you. For example, if you tap on Show More, you will see the widget expand and show more items. If you tap on Edit you will see another view controller pop up for editing the widget list.

Of course, the app is just a simulation of the lock screen in iOS. It doesn’t actually perform any actions, and is set up specially for you to play with some UIViewPropertyAnimator animations. Let’s go!

First, you are going to create a very simple animation starting when the app initially opens up. Open LockScreenViewController.swift and add a new viewWillAppear(_:) method to that view controller:

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  tableView.transform = CGAffineTransform(scaleX: 0.67, y: 0.67)
  tableView.alpha = 0
}

The table view in LockScreenViewController displays the widgets on that screen, so in order to create a simple scale and fade view animation transition, you first scale the whole table view down and make it transparent.

If you run the project right now you will see the date and some empty space below:

Next, create an animator when the view controller’s view appears on screen. Add the following to LockScreenViewController:

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  let scale = UIViewPropertyAnimator(duration: 0.33,
    curve: .easeIn)
}

Here, you use one of the convenience initializers of UIViewPropertyAnimator. You are going to try all of them, but you’ll start with the simplest one: UIViewPropertyAnimator(duration:, curve:).

This initializer makes an animator instance and sets the animation’s total duration and timing curve. The latter parameter is of type UIViewAnimationCurve, and this is an enum with the following curve-based options:

  • easeInOut
  • easeIn
  • easeOut
  • linear

These match the timing options that you’ve used with the UIView.animate(withDuration:...) APIs, and they produce similar results.

Now that you’ve created an animator object, let’s have a look at what can you do with it.

Adding Animations

Add the animation code to viewDidAppear(_:):

scale.addAnimations {
  self.tableView.alpha = 1.0
}
scale.addAnimations({
  self.tableView.transform = .identity
}, delayFactor: 0.33)
delayFactor(0.33) * remainingDuration(=duration 0.33) = delay of 0.11 seconds

Adding Completions

Now add a completion block, just like you’re used to with UIView.animate(withDuration:...):

scale.addCompletion { _ in
  print("ready")
}
scale.startAnimation()

Abstracting Animations Away

You’ve probably already noticed that just like layer animations, animations with UIViewPropertyAnimator add quite a bit of code.

import UIKit

enum AnimatorFactory {
}
static func scaleUp(view: UIView) -> UIViewPropertyAnimator {
  let scale = UIViewPropertyAnimator(duration: 0.33, 
    curve: .easeIn)
  scale.addAnimations {
    view.alpha = 1.0
  }
  scale.addAnimations({
    view.transform = CGAffineTransform.identity
  }, delayFactor: 0.33)
  scale.addCompletion { _ in
    print("ready")
  }
  return scale
}
override func viewDidAppear(_ animated: Bool) {
  AnimatorFactory.scaleUp(view: tableView)
    .startAnimation()
}

Running Animators

At this point you might be asking yourself “What’s the point of creating an animator object if its only purpose is to be started right away?” That is a good question!

func toggleBlur(_ blurred: Bool) {
  UIViewPropertyAnimator.runningPropertyAnimator(
    withDuration: 0.5, delay: 0.1, options: .curveEaseOut, 
    animations: {
      self.blurView.alpha = blurred ? 1 : 0
    }, 
    completion: nil
  )
}
extension LockScreenViewController: UISearchBarDelegate {

  func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
    toggleBlur(true)
  }

  func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
    toggleBlur(false)
  }
}
func searchBarResultsListButtonClicked(_ searchBar: UISearchBar) {
  searchBar.resignFirstResponder()
}

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
  if searchText.isEmpty {
    searchBar.resignFirstResponder()
  }
}

Basic Keyframe Animations

Earlier you learned how to add more animation blocks to the same animator and make them start with a given delay factor. This is handy, but it doesn’t do quite the same thing as you’re used to with using view keyframe animations.

static func jiggle(view: UIView) -> UIViewPropertyAnimator {
  return UIViewPropertyAnimator.runningPropertyAnimator(
    withDuration: 0.33, delay: 0, animations: {

    },
    completion: { _ in

    }
  )
}
UIView.animateKeyframes(withDuration: 1, delay: 0,
  animations: {

    UIView.addKeyframe(withRelativeStartTime: 0.0,
      relativeDuration: 0.25) {
      view.transform = CGAffineTransform(rotationAngle: -.pi / 8)
    }
    UIView.addKeyframe(withRelativeStartTime: 0.25,
      relativeDuration: 0.75) {
      view.transform = CGAffineTransform(rotationAngle: +.pi / 8)
    }
    UIView.addKeyframe(withRelativeStartTime: 0.75,
      relativeDuration: 1.0) {
      view.transform = CGAffineTransform.identity
    }
  },
  completion: nil
)
view.transform = .identity

func iconJiggle() {
  AnimatorFactory.jiggle(view: icon)
}
@discardableResult
static func jiggle(view: UIView) -> UIViewPropertyAnimator
if let cell = collectionView.cellForItem(at: indexPath) as? IconCell {
  cell.iconJiggle()
}

Key Points

  • When using the UIViewPropertyAnimator type, you create an object, add animation and/or completion closures to it, and start the animations at your convenience.
  • Containing animation closures within class instances provides a whole new approach to reusing animations via animation factories.
  • To create view keyframe animations, you still need to use the old api UIView.animateKeyframes(withDuration:delay:keyframes:).

Challenges

You already know some of the basics about working with UIViewPropertyAnimator, but there’s much more to learn in the next three chapters. In this chapter’s challenge section, take the time to reflect on what you’ve learned and experience your first encounter with property animator’s state.

Challenge 1: Extract Blur Animation into Factory

To practice abstracting animations one more time, extract the blur animation from toggleBlur(_:) into a static method on AnimatorFactory.

func toggleBlur(_ blurred: Bool) {
  AnimatorFactory.fade(view: blurView, visible: blurred)
}

Challenge 2: Prevent Overlapping Animations

In this challenge, you will learn how to check if an animator is currently executing its 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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now