Advances in UIKit Animations and Transitions

Description: Direct onscreen manipulation is the cornerstone of the user experience on iOS. iOS 10 includes new support for making onscreen interactions even more immersive and interactive. Dive straight into the philosophy and techniques of building completely interactive, interruptible animations in your apps.

UIViewPropertyAnimator

Features

  • Familiar
  • Interruptible
  • Scrubbable
  • Reversible
  • Broad availability of timing functions
  • Running animations can be modified

Introduction

UIViewPropertyAnimator conform to two protocols: UIViewImplicitlyAnimating and UIViewAnimating.

Thanks to these conformances, UIViewPropertyAnimator can be used for view controller transitions.

When creating a new animator, you're also creating a new object conforming to UITimingCurveProvider, which dictates the timing function that you want that animation to use.

UIViewAnimating protocol definition:

protocol UIViewAnimating {
  var state: UIViewAnimatingState { get }

  /// whether the animation is running or not
  var isRunning: Bool { get }

  /// whether the animation is running in the forward or reverse direction
  var isReversed: Bool { get set }
  var fractionComplete: CGFloat { get set }

  func startAnimation()
  func startAnimation(afterDelay : TimInterval)

  func pauseAnimation()

  func stopAnimation(_ withoutFinishing: Bool)
  func finishAnimation(at finalPosition: UIViewAnimatingPosition)
}

UIViewImplicitlyAnimating adds the implicit characteristics to this animator:

protocol UIViewImplicitlyAnimating {
  optional func addAnimations(_ animation: () -> Void, delayFactor: CGFloat)
  optional func addAnimations(_ animation: () -> Void)

  optional func addCompletion(_ completion: (UIViewAnimatingPosition) -> Void)

  /// Allows you to proceed from a paused animation with a completely different 
  /// finish duration, and possibly even a different timing function.
  optional func continueAnimation(
    withTimingParameters parameters: UITimingCurveProvider?, 
    durationFactor: CGFloat
  )
}

UIViewPropertyAnimator definition:

class UIViewPropertyAnimator {
  var timingParameters: UITimingCurveProvider? { get }
  var duration: TimeInterval { get }
  var delay: TimeInterval { get }
  var isUserInteractionEnabled: Bool { get set }
  var isManualHitTestingEnabled: Bool { get set }
  var isInterruptible: Bool { get set }
  
  init(
    duration: TimeInterval, 
    timingParameters parameters: UITimingCurveProvider
  )

  class func runningPropertyAnimator(
    withDuration duration: TimeInterval,
    delay: TimeInterval, options: UIViewAnimationOptions = [],
    animations: (() -> Void)?,
    completion: ((UIViewAnimatingPosition) -> Void)? = nil
  ) -> Self
}

Basic usage

// define your timing function
let timing = UICubicTimingParameters(animationCurve: .easeInOut)

// create the animator
let animator = UIViewPropertyAnimator(duration: 2.0, timingParameters:timing)

// add your animations
animator.addAnimations {
  self.squareView.center = CGPoint(x: 800.0, y: self.squareView.center,y)
  self.squareView.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI_2))
}

// add optional completion block to be called
animator.addCompletion {_ in
  self.squareView.backgroundColor = UIColor.orange()
}

// trigger the animation
animator.startAnimation()

Pausing and reversing

The cool part of having this UIViewPropertyAnimator object is that now it's easy to, for example, pause the animation, reverse it, and start again:

animator.pauseAnimation()
animator.isReversed = true
/// will go backwards
animator.startAnimation()

Note that the completion block will be called with a UIViewAnimatingPosition instance, telling us where we are at when the animation has completed (possible values: .start, .end, .current)

Stop animating

UIViewPropertyAnimator also comes with stopAnimation(_:), what happens is that we update the actual view model value to the animating value at that instant.

The withoutFinishing parameter tells whether we should consider the animation completed or not:

  • If we pass false, the animation is not completed and it is expected to call finishAnimation(at:) in the future to complete the animation (at that point the animator completion block is also called). Note that calling finishAnimation(at:) will not have any animation, it's up to you to make the final animation and then call finishAnimation(at:)
animator.stopAnimation(false)
animator.finishAnimation(.current)
// completion(.current) called
  • If we pass true, the animation ends right there and the object completion block is not called
animator.stopAnimation(true)
// completion(_) not called

Reversing

Three ways:

  • pause animation, reverse, start animation (as we've seen in pausing chapter)
  • reverse (change isReversed value) while the animation is running
  • add a new animation to go back to the original position, for example:
animator.addAnimations {
  // go back to initial state, for example:
  view.center.x = 150.0
  view.transform = CGAffineTransform.identity
}

This last way is preferred as the final animation is more fluid. Note that, with this last way, the completion handler will be called with .end parameter, as the animation ends in the position that we just specified.

Custom View Controller Transitions

See Apple's article here for more.

View controller transitions basically are a bunch of interlocking protocols:

UIkit gets the objects that conform to these protocols via a delegate: it might be a UINavigationControllerDelegate, or it might be a view controller UIViewControllerTransitioningDelegate.
If any of these objects can return an animated transitioning object:

  • UIKit will bypass the built-in transition
  • (for non-interactive transitions) UIKit will call animateTransition(using:) and pass a UIViewControllerContextTransitioning object for you to use
  • (for interactive transitions) UIKit will call interruptibleAnimator(using:) and pass a UIViewControllerContextTransitioning object for you to use

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.