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

21. Interactive UINavigationController Transitions
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

The reveal transition you created in the previous chapter looks pretty neat, but custom animations are only half the story. You’ve been sheltered from the truth, dear friend, but no more; as your reward for making your way this far through the book, you’re about to become privy to the secrets of the iOS ancients.

Not only can you create a custom animation for your transition — you can also make it interactive and respond to the actions of the user.

Typically, you’d drive this action through a pan gesture, which is the approach you’re going to take in this chapter.

When you’re done, your users will be able to scrub back and forth through the reveal transition by sliding their finger across the screen. How cool would that be?

Yeah, I thought you’d be interested! Read on to see how it’s done!

Creating an Interactive Transition

When your navigation controller asks its delegate for an animation controller, two things can happen. You can return nil, in which case the navigation controller runs the standard transition animation. You know that much already. However — if you do return an animation controller, then the navigation controller asks its delegate for an interaction controller like so:

The interaction controller moves the transition along based on the user’s actions, instead of simply animating the changes from start to finish.

The interaction controller does not necessarily need to be a separate class from the animation controller; in fact, performing some tasks is a little easier when both controllers are in the same class. You just need to make sure that said class conforms to both UIViewControllerAnimatedTransitioning and UIViewControllerInteractiveTransitioning.

UIViewControllerInteractiveTransitioning has only one required method — startInteractiveTransition(_:) — that takes a transitioning context as its parameter. The interaction controller then regularly calls updateInteractiveTransition(_:) to move the transition along. To begin, you’ll need to change how you handle your user input.

Handling the Pan Gesture

First of all, the tap gesture recognizer in MainViewController just won’t cut it anymore. A tap happens momentarily and then it’s gone; you can’t track its progress and use it to drive a transition. On the other hand, a pan gesture has clear states for the starting, progressing, and ending phases of the transition.

let tap = UITapGestureRecognizer(target: self, action: #selector(didTap))
view.addGestureRecognizer(tap)
let pan = UIPanGestureRecognizer(target: self, action: #selector(didPan(_:)))
view.addGestureRecognizer(pan)
@objc func didPan(_ recognizer: UIPanGestureRecognizer) {
}

Using Interactive Animator Classes

To manage your transition, you’ll use one of Apple’s built-in interactive animator classes: UIPercentDrivenInteractiveTransition. This class conforms to UIViewControllerInteractiveTransitioning and lets you get and set your transition’s progress as a value representing the percentage complete.

class RevealAnimator: UIPercentDrivenInteractiveTransition,
  UIViewControllerAnimatedTransitioning, CAAnimationDelegate {
var interactive = false
func handlePan(_ recognizer: UIPanGestureRecognizer) {

}
func navigationController(
  _ navigationController: UINavigationController,
  interactionControllerFor
  animationController: UIViewControllerAnimatedTransitioning
) -> UIViewControllerInteractiveTransitioning? {
  if !transition.interactive {
    return nil
  }
  return transition
}
@objc func didPan(_ recognizer: UIPanGestureRecognizer) {
  switch recognizer.state {
  case .began:
    transition.interactive = true
    performSegue(withIdentifier: "details", sender: nil)
  default:
    transition.handlePan(recognizer)
  }
}

Calculating Your Animation’s Progress

The most important bit of your pan gesture handler is to figure out how far along the transition should be.

let translation = recognizer.translation(
  in: recognizer.view?.superview)
var progress: CGFloat = abs(translation.x / 200.0)
progress = min(max(progress, 0.01), 0.99)
switch recognizer.state {
case .changed:
  update(progress)
default:    
  break
}
private var pausedTime: CFTimeInterval = 0
private var isLayerBased: Bool {
  return operation == .push
}
if interactive && isLayerBased {
  let transitionLayer = transitionContext.containerView.layer
  pausedTime = transitionLayer.convertTime(CACurrentMediaTime(), from: nil)
  transitionLayer.speed = 0
  transitionLayer.timeOffset = pausedTime
}
override func update(_ percentComplete: CGFloat) {
  super.update(percentComplete)
  if isLayerBased {
    let animationProgress = TimeInterval(animationDuration) * TimeInterval(percentComplete)
    storedContext?.containerView.layer.timeOffset = 
      pausedTime + animationProgress
  }
}

Handling Early Termination

Here you face a totally new problem: the user might lift their finger before they’ve panned 200 points on the X axis. This leaves the transition in an unfinished state.

case .cancelled, .ended:
  if progress < 0.5 {
    cancel()
  } else {
    finish()
  }

override func cancel() {
  if isLayerBased {
    restart(forFinishing: false)
  }
  super.cancel()
}

override func finish() {
  if isLayerBased {
    restart(forFinishing: true)
  }
  super.finish()
}

private func restart(forFinishing: Bool) {
  let transitionLayer = storedContext?.containerView.layer
  transitionLayer?.beginTime = CACurrentMediaTime()
  transitionLayer?.speed = forFinishing ? 1 : -1
}
interactive = false

Key Points

  • By adopting the UIPercentDrivenInteractiveTransition protocol in your transition animator, you can easily add interactivity to your custom transitions.
  • Interactive transitions are usually driven by user gestures. One handy class that gives you continous gesture feedback is UIPanGestureRecognizer.
  • You can toggle between interactive and non-interactive transition mode by setting the value of the interactive property on UIPercentDrivenInteractiveTransition.

Challenges

The final challenge is a bit more difficult than usual, but by now you’re an animation ninja, and I know you can handle anything I throw at you!

Challenge 1: Make the Pop Transition Interactive

Your task in this challenge is to make the pop transition interactive. That’s not as easy as it sounds, as you’ll need to change code in a number of places throughout the project.

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