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

25. UIViewPropertyAnimator View Controller 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

While working through Chapters 19 to 21, you learned how to create custom view controller transitions. You saw how flexible and powerful those can be, so naturally you are probably craving to know how to use UIViewPropertyAnimator to create them as well.

Good news — using an animator for your transitions is pretty easy, and there are almost no surprises there.

In this chapter, you are going to review building custom transition animations and create both static and interactive transitions for your Widgets project.

When you’ve finished working through the chapter, your users will be able to scrub through presenting the settings view controller by pulling down the widget table.

If you worked on the challenges from the last chapter, keep working on the same project; if you skipped over the challenges, open the starter project provided for this chapter.

Static View Controller Transitions

Currently, the experience is pretty stale when the user taps the “Edit” button. The button presents a new view controller on top of the current one, and as soon as you tap any of the available options in that second screen, it disappears.

Let’s spice that up a notch!

Create a new file and name it PresentTransition.swift. Replace its default contents with:

import UIKit

class PresentTransition: NSObject,
  UIViewControllerAnimatedTransitioning {
  func transitionDuration(
    using transitionContext: UIViewControllerContextTransitioning?
  ) -> TimeInterval {
    return 0.75
  }

  func animateTransition(
    using transitionContext: UIViewControllerContextTransitioning
  ) {
  }
}

You are familiar with the UIViewControllerAnimatedTransitioning protocol, so you should hopefully be familiar with this piece of code.

Note: In case you skipped the View Controller Transitions section of the book, I’d recommend taking a step back and working through at least Chapter 19, “Presentation Controller & Orientation Animations”.

In this part of the chapter, you are going to create a transition animation that animates the blur layer and moves the new view controller on top of it.

Add the following method, in the same file you have open, to create an animator for the transition:

func transitionAnimator(using transitionContext: 
  UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
  let duration = transitionDuration(using: transitionContext)

  let container = transitionContext.containerView
  guard let toView = transitionContext.view(forKey: .to) else {
    return UIViewPropertyAnimator()
  }

  container.addSubview(toView)
}

In the code above, you make all necessary preparations for the view controller transition. You begin by getting the animation duration, you then fetch the target view controller’s view, and finally add this view to the transition container.

Next you can set up the animation and run it. Add this code to transitionAnimator(using:) to prepare the UI for the transition animation:

toView.transform = CGAffineTransform(scaleX: 1.33, y: 1.33)
  .concatenating(CGAffineTransform(translationX: 0.0, y: 200))
toView.alpha = 0

This scales up and moves down the target view controller’s view and fades it out. Now it’s ready to be animated onto the screen.

Add the animator after toView.alpha = 0 to run the transition:

let animator = UIViewPropertyAnimator(duration: duration, curve: .easeOut)

animator.addAnimations({
  toView.transform = CGAffineTransform(translationX: 0.0, y: 100)
}, delayFactor: 0.15)

animator.addAnimations({
  toView.alpha = 1.0
}, delayFactor: 0.5)

In this code, you create an animator with two animation blocks:

  1. The first animation moves the target view controller’s view to its final position.
  2. The second animation fades the content in from an alpha of 0 to 1.

As in the previous chapters you should never forget to wrap up the transition. Add a completion to the animator:

animator.addCompletion { _ in
  transitionContext.completeTransition(
    !transitionContext.transitionWasCancelled
  )
}

Once your animations complete, you let UIKit know that you’re finished transitioning. At the end of your method simply return the animator:

return animator

Now that you have your animator factory method, you have to also use it. Scroll up to animateTransition(using:) and insert this code:

transitionAnimator(using: transitionContext).startAnimation()

This will fetch a ready-to-go animator, and begin via startAnimation(). That should do it for the time being. Let’s wire up the view controller to the transition animator and give the animation a try.

Open LockScreenViewController and define the following constant property:

let presentTransition = PresentTransition()

You will provide this object to UIKit when it asks you for a presentation animation and interaction controller. To do that, add a UIViewControllerTransitioningDelegate conformance to LockScreenViewController:

extension LockScreenViewController: UIViewControllerTransitioningDelegate {
  func animationController(
    forPresented presented: UIViewController,
    presenting: UIViewController,
    source: UIViewController
  ) -> UIViewControllerAnimatedTransitioning? {
    return presentTransition
  }
}

The animationController(forPresented:presenting:source:) method is where you have your chance to tell UIKit that you’re planning on spawning a new custom view controller transition. You return the presentTransition from that method and UIKit uses it for the animations to follow.

Now for the last step — you need to set LockScreenViewController as the presentation delegate. Scroll to presentSettings(_:), and just before calling present(_:animated:completion:) set self as the transition delegate:

settings.transitioningDelegate = self

This should be it! Run the app and tap on the Edit button to try the transition.

The initial result isn’t all that exciting (at least not yet!). The settings controller seems to be a bit off:

You’ll want to take care of few rough edges, but your job here is almost finished. The first thing to correct is the target view controller doesn’t need the solid background color.

Open Main.storyboard (it’s in the Assets project folder) and select the settings view controller view.

Change the view’s Background to Clear Color and you should see the storyboard reflect that change like so:

Give that transition another try. This time you should see the contents of the settings view controller appear directly over the lock screen:

It looks like this transition can do with a few more animations. Wouldn’t it be nice, for example, to fade in the blur on top of the widget so that the user can see better the modal view controller on top?

Since you’re a pro already, let’s do something new — “animation injection”! (No need to look that term up — I just came up with it for this chapter).

You will add a new property to the animator that will allow you to inject any custom animation into the transition. This will allow you to use the same transition class to produce slightly different animations.

Switch to PresentTransition.swift and add a new property:

var auxAnimations: (() -> Void)?

Append this to the bottom of transitionAnimator(using:), just before return:

if let auxAnimations = auxAnimations {
  animator.addAnimations(auxAnimations)
}

In case you’ve added any arbitrary block of animations to the object, they will be added to the rest of the animator’s animations.

This allows you to, depending on the situation, add custom animations into the transition. For example, let’s add a blur animation to the current transition.

Open LockScreenViewController and insert the following at the top of presentSettings():

presentTransition.auxAnimations = blurAnimations(true)

This will add the blur animation you created many chapters ago to the view controller transition!

Give the transition another try and see how that one line changed it:

Isn’t reusing animations simply amazing?

Now you also need to hide the blur when the user dismisses the presented controller. SettingsViewController already has a didDismiss property so you simply need to set that property to a block that animates the blur out.

In presentSettings(_:) on the second-to-last line before settings is presented, insert:

settings.didDismiss = { [unowned self] in
  self.toggleBlur(false)
}

Now tapping on one of the options in the settings screen will dismiss it. The blur will then disappear and the user will be successfully taken back to the first view controller:

This concludes this part of the chapter. Your view controller transition is ready!

Interactive View Controller Transitions

As the final topic in the UIViewPropertyAnimator section of the book, you are going to create an interactive view controller transition. Your user will drive the transition by pulling down the widget table.

class PresentTransition: NSObject,
  UIViewControllerAnimatedTransitioning {
class PresentTransition: UIPercentDrivenInteractiveTransition, 
  UIViewControllerAnimatedTransitioning {
var animator: UIViewPropertyAnimator?
self.animator = animator

animator.addCompletion { [unowned self] _ in
  self.animator = nil
}
func interruptibleAnimator(
  using transitionContext: UIViewControllerContextTransitioning
) -> UIViewImplicitlyAnimating {
  return animator ?? transitionAnimator(using: transitionContext)
}

func interactionControllerForPresentation(
  using animator: UIViewControllerAnimatedTransitioning
  ) -> UIViewControllerInteractiveTransitioning? {
  return presentTransition
}
var isDragging = false
var isPresentingSettings = false
extension LockScreenViewController: UIScrollViewDelegate {
  func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    isDragging = true
  }
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
  guard isDragging else {
    return
  }

  if !isPresentingSettings && scrollView.contentOffset.y < -30 {
    isPresentingSettings = true
    presentTransition.wantsInteractiveStart = true
    presentSettings()
    return
  }
}
if isPresentingSettings {
  let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
  presentTransition.update(progress)
}

func scrollViewWillEndDragging(
  _ scrollView: UIScrollView,
  withVelocity velocity: CGPoint,
  targetContentOffset: UnsafeMutablePointer<CGPoint>
) {
  let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))

  if progress > 0.5 {
    presentTransition.finish()
  } else {
    presentTransition.cancel()
  }

  isPresentingSettings = false
  isDragging = false
}
animator.addCompletion { position in
  switch position {
  case .end:
    transitionContext.completeTransition(
      !transitionContext.transitionWasCancelled)
  default:
    transitionContext.completeTransition(false)
  }
}
(cell as? FooterCell)?.didPressEdit = { [unowned self] in
  self.presentSettings()
}
self.presentTransition.wantsInteractiveStart = false

Interruptible Transition Animations

Next you are going to look into switching between non-interactive and interactive modes during the transition. The integration of UIViewPropertyAnimator with view controller transitions aims to solve the issues around situations where the user starts the transition to another controller, but changes their mind mid-way.

animator.isUserInteractionEnabled = true
var touchesStartPointY: CGFloat?
override func touchesBegan(
  _ touches: Set<UITouch>,
  with event: UIEvent?
) {
  guard presentTransition.wantsInteractiveStart == false, 
    presentTransition.animator != nil else {
    return
  }

  touchesStartPointY = touches.first?.location(in: view).y
  presentTransition.pause()
}

override func touchesMoved(
  _ touches: Set<UITouch>,
  with event: UIEvent?
) {
  guard
    let startY = touchesStartPointY,
    let currentPoint = touches.first?.location(in: view).y
  else {
    return
  }

  if currentPoint < startY - 40 {
    touchesStartPointY = nil
    presentTransition.animator?.addCompletion { _ in
      self.blurView.effect = nil
    }
    presentTransition.cancel()
  } else if currentPoint > startY + 40 {
    touchesStartPointY = nil
    presentTransition.finish()
  }
}

Key Points

  • By combining your knowledge about custom transitions and creating interactive, interruptible animations with UIViewPropertyAnimator, you can create stunning transition effects.
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