Chapters

Hide chapters

iOS Animations by Tutorials

Sixth Edition · iOS 13 · Swift 5.1 · Xcode 11

Section IV: Layer Animations

Section 4: 9 chapters
Show chapters Hide chapters

24. Interactive Animations 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

You’ve already covered a lot of the UIViewPropertyAnimator APIs such as basic animations, custom timings and springs, and abstracting of animations. But you haven’t yet looked into what makes this class really interesting compared to the old style “fire-and-forget” APIs.

UIView.animate(withDuration:...) offers a way to animate views on screen, but once you’ve defined the desired end state, the animations are sent off for rendering and control is out of your hands.

But what if you wanted to interact with the animations? Or to create animations, which aren’t static but are driven by user gestures or microphone input like you did in the part of the book covering layer animations?

This is where UIViewPropertyAnimator really comes through in regard to animating views. The animations created with this class are fully interactive: you can start, pause them, and alter their speed. Finally you can simply “scrub-through” the animation by directly setting the current progress.

Since UIViewPropertyAnimator can drive both preset animations and interactive animations, things get a bit complicated when it comes to telling what state an animator is currently. The next part of the chapter will teach you how to deal with animator state.

If you have completed the challenge in the previous chapter, just keep working on your Xcode project; if you skipped over the challenge, open the starter project provided for this chapter.

You should have the project featuring different animations which kick in when you enter text in the search bar, tap on an icon, or expand the widget view.

An animation state machine

Besides taking care of your animations, UIViewPropertyAnimator exhibits behaviors of a state machine, and can give you information about many different aspects of the current state of your animations.

You can check if an animation has started, if it has been paused or completely stopped, or whether the animation has been reversed. And finally, you can check where the animation “completed”, such as at the desired end state, from the beginning, or somewhere in between.

There are three properties on UIViewPropertyAnimator that help you figure out the current state:

The isRunning property (read-only) tells you if the animator’s animations are currently in motion. The property is false by default and becomes true when startAnimation() is called. It becomes false again if you pause or stop the animations, or your animations complete naturally.

The isReversed property is, by default, false since you always start your animations in forward direction, i.e. your animation plays from its start state to its end state. If you change this property to true, the animation will reverse direction and play back to its initial state.

The state property (read-only) determines whether the animator is active and currently animating, or in some other passive state.

By default, state is inactive. This usually means you’ve just created the animator and haven’t called any methods on it yet. Please note that this is not the same as having isRunning set to false: isRunning is really only concerned with animations being played, while when state is inactive that really means that the animator hasn’t done anything much yet.

state becomes active when you either:

  • Call startAnimation() to start your animations
  • Call pauseAnimation() without even starting your animations first,
  • Set the fractionComplete property to “rewind” the animation to a certain position.

Once your animations complete naturally, state switches back to inactive.

If you call stopAnimation() on your animator, it will set its state property to stopped. In this state, the only thing you could do is either abandon the animator altogether or call finishAnimation(at:) to complete the animations and bring the animator back to the inactive state.

As you probably figured out, UIViewPropertyAnimator can only switch between states in a certain sequence. It can’t go straight from inactive to stopped, nor from stopped to active.

There is one more option under your control: if you set the property called pausesOnCompletion, once the animator has finished running its animations instead of stopping itself it will pause. This will give you the opportunity to continue working with it from a paused state.

If you’re in doubt, you can always come back to this part of the chapter and consult the state flow diagram below:

Don’t worry if managing the state with UIViewPropertyAnimator sounds a bit complicated at first. Should you call a method you’re not allowed to call in the current state, your app will immediately crash so you will have the chance to figure out where you went wrong.

Interactive 3D touch animation

In this part of the chapter, you are going to create an interactive animation similar to the 3D touch interaction on your iPhone home screen:

var startFrame: CGRect?
var previewView: UIView?
var previewAnimator: UIViewPropertyAnimator?
let previewEffectView = IconEffectView(blur: .extraLight)

func startPreview(for forView: UIView) {
  previewView?.removeFromSuperview()
  previewView = forView.snapshotView(afterScreenUpdates: false)
  view.insertSubview(previewView!, aboveSubview: blurView)
}

previewView?.frame = forView.convert(forView.bounds, to: view)
startFrame = previewView?.frame
addEffectView(below: previewView!)
func addEffectView(below forView: UIView) {
  previewEffectView.removeFromSuperview()
  previewEffectView.frame = forView.frame

  forView.superview?.insertSubview(previewEffectView,
    belowSubview: forView)
}
static func grow(view: UIVisualEffectView, 
  blurView: UIVisualEffectView) -> UIViewPropertyAnimator {

  // 1
  view.contentView.alpha = 0
  view.transform = .identity

  // 2
  let animator = UIViewPropertyAnimator(
    duration: 0.5, curve: .easeIn)

  return animator
}
// 3
animator.addAnimations {
  blurView.effect = UIBlurEffect(style: .dark)
  view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}

// 4
animator.addCompletion { _ in
  blurView.effect = UIBlurEffect(style: .dark)
}
previewAnimator = AnimatorFactory.grow(view: previewEffectView, 
  blurView: blurView)
func updatePreview(percent: CGFloat) {
  previewAnimator?.fractionComplete = 
    max(0.01, min(0.99, percent))
}

static func reset(frame: CGRect, view: UIVisualEffectView,
  blurView: UIVisualEffectView) -> UIViewPropertyAnimator {

  return UIViewPropertyAnimator(duration: 0.5, 
    dampingRatio: 0.7) {

    view.transform = .identity
    view.frame = frame
    view.contentView.alpha = 0

    blurView.effect = nil
  }
}
func cancelPreview() {
  if let previewAnimator = previewAnimator {
    previewAnimator.isReversed = true
    previewAnimator.startAnimation()
  }
}

animator.addCompletion { _ in
  blurView.effect = UIBlurEffect(style: .dark)
}
animator.addCompletion { position in
  switch position {
    case .start:
      blurView.effect = nil
    case .end:
      blurView.effect = UIBlurEffect(style: .dark)
    default: break
  }
}
previewAnimator.addCompletion { position in
  switch position {
    case .start:
      self.previewView?.removeFromSuperview()
      self.previewEffectView.removeFromSuperview()
    default: break
  }
}
static func complete(view: UIVisualEffectView) -> UIViewPropertyAnimator {
  return UIViewPropertyAnimator(duration: 0.3, 
    dampingRatio: 0.7) {
    view.contentView.alpha = 1
    view.transform = .identity
    view.frame = CGRect(
      x: view.frame.minX - view.frame.minX/2.5,
      y: view.frame.maxY - 140,
      width: view.frame.width + 120,
      height: 60
    )
  }
}

func finishPreview() {
  // 1
  previewAnimator?.stopAnimation(false)

  // 2
  previewAnimator?.finishAnimation(at: .end)

  // 3
  previewAnimator = nil
}
AnimatorFactory.complete(view: previewEffectView)
  .startAnimation()

Key points

Challenges

Challenge 1: Allow the users to dismiss the menu

Once the user sees the complete animation which displays the menu, they can’t do anything else with the app.

blurView.effect = UIBlurEffect(style: .dark)
blurView.isUserInteractionEnabled = true
blurView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(dismissMenu)))

Challenge 2: Interactive keyframe animations

In chapter 22 you learned how easy it is to add keyframe animations to an animator. If you have an animator with keyframes, you can still use it to create interactive animations. Your users can scrub through the keyframes back and forth.

animator.addAnimations {
  blurView.effect = UIBlurEffect(style: .dark)
  view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}
animator.addAnimations {
  UIView.animateKeyframes(withDuration: 0.5, delay: 0.0, animations: {

    UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1.0, animations: {
      blurView.effect = UIBlurEffect(style: .dark)
      view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
    })

    UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
      view.transform = view.transform.rotated(by: -.pi/8)
    })
  })
}

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