25.
UIViewPropertyAnimator View Controller Transitions
Written by Marin Todorov
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:
- The first animation moves the target view controller’s view to its final position.
- The second animation fades the content in from an
alphaof 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.
First and foremost, let’s use the powerful UIPercentDrivenInteractionTransition class to enable interactivity for the view controller transition. Open PresentTransition.swift and replace:
class PresentTransition: NSObject,
UIViewControllerAnimatedTransitioning {
With:
class PresentTransition: UIPercentDrivenInteractiveTransition,
UIViewControllerAnimatedTransitioning {
UIPercentDrivenInteractiveTransition is a class that defines the “percent” based transition methods such as:
-
update(_:)to rewind through the transition. -
cancel()to cancel the view controller transition. -
finish()to play the the transition until it completes.
You may remember these from Chapter 21, “Interactive UINavigationController Transitions”, but you didn’t look at some of the advanced APIs that accommodate using a UIViewPropertyAnimator specifically. Some of the new functionally built to make animator transitions easier include:
-
timingCurve: In case your user drives the transition interactively and lets go at a point when you need to play the transition till the end, you can provide a custom timing curve for the animation by setting this property. This can be a cubic, spring, or another custom timing provider. -
wantsInteractiveStart: By default this istruesince you are probably going to use this class mostly for interactive transitions. However, if you set the property tofalse, the transition will start non-interactively and you could pause it and go to interactive mode at a later point. -
pause(): Call this method to pause a non-interactive transition and switch to interactive mode.
For an interactive animation, you need to keep track of the animator object rather than keep making new ones. Add a new property to PresentTransition:
var animator: UIViewPropertyAnimator?
Then, in transitionAnimator(using:), just before the return statement, add this code:
self.animator = animator
animator.addCompletion { [unowned self] _ in
self.animator = nil
}
This line stores the animator object that you create for the non-interactive transition, and removes it once the animation is complete. Now, add a new method to PresentTransition:
func interruptibleAnimator(
using transitionContext: UIViewControllerContextTransitioning
) -> UIViewImplicitlyAnimating {
return animator ?? transitionAnimator(using: transitionContext)
}
This is a method on the UIViewControllerAnimatedTransitioning protocol. It allows you to provide to UIKit an interruptible animator, which it will use for your transition animations. It can get called multiple times, and it’s important that you return the same animator each time, so you only create a new animator object if one doesn’t already exist.
Your transition animator class has now two different behaviors:
-
If it is used non-interactively (when the user presses the Edit button) UIKit will call
animateTransition(using:)to animate the transition. -
If it is used interactively, UIKit will call
interruptibleAnimator(using:), get your animator, and use it to drive the transition that way.
Switch to LockScreenViewController.swift and add this new method inside the UIViewControllerTransitioningDelegate extension:
func interactionControllerForPresentation(
using animator: UIViewControllerAnimatedTransitioning
) -> UIViewControllerInteractiveTransitioning? {
return presentTransition
}
This will let UIKit know you’re planning some playful interactiveness during the view controller presentation.
Next, still in LockScreenViewController.swift, add two new properties; you will need them to keep track of the user’s gesture:
var isDragging = false
var isPresentingSettings = false
As the user pulls the table down you will set the isDragging flag to true, and once the user has pulled far enough, you will set isPresentingSettings to true in turn.
To track how far has the user pulled the table view, you will need to implement some of its scroll view delegate methods. Add a new extension and insert the first of those methods:
extension LockScreenViewController: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
isDragging = true
}
}
This might seem a bit redundant since UITableView already has a property to track if it’s being currently dragged, but this time you are going to do some custom tracking yourself.
Next add the delegate method to track the user’s progress:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard isDragging else {
return
}
if !isPresentingSettings && scrollView.contentOffset.y < -30 {
isPresentingSettings = true
presentTransition.wantsInteractiveStart = true
presentSettings()
return
}
}
First, you check if your isDragging flag is enabled; you are not interested in tracking the table view’s offset otherwise. Then you check if the user has pulled far enough to start the transition.
If both conditions are true, you prepare the transition setup. You set isPresentingSettings to true, you set the transition animator to interactive mode, and finally you call presentSettings().
presentSettings() takes care to start the view controller transition in interactive mode because you set the wantsInteractiveStart to true in advance.
Next, you need to add the code to update it interactively. Append the following to the end of scrollViewDidScroll(_:):
if isPresentingSettings {
let progress = max(0.0, min(1.0, ((-scrollView.contentOffset.y) - 30) / 90.0))
presentTransition.update(progress)
}
You calculate a progress in the range 0.0 to 1.0 based on how far the user has pulled the table view and call update(_:) on the transition animator to position the animation at the current progress.
Give the transition a try right now, and you will see the table view blur progressively as you drag it down.
You also need to take care of completing and canceling the transition. Add to the same extension as before:
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
}
This code should look similar; it’s the same approach you employed in Chapter 21, “Interactive UINavigationController Transitions”.
If the user has pulled through more than half of the distance (that you decided to be “far enough”) you consider the transition successful and play the animation to the end. If the user hasn’t dragged more than half the distance, you cancel the transition. Either way, you reset the values of the two flags and the interactive part of the transition is over.
The transition, however, is not complete just yet — there are few more things to polish before it’s production ready.
Switch to PresentTransition.swift and find transitionAnimator(using:). In the completion block, you ignore the parameter and always call completeTransition(_:) with the same value.
You can help UIKit by checking at which position the animator completed and provide the relevant value. Replace the existing call to addCompletion(...) where you complete the transition, with:
animator.addCompletion { position in
switch position {
case .end:
transitionContext.completeTransition(
!transitionContext.transitionWasCancelled)
default:
transitionContext.completeTransition(false)
}
}
Indeed the view controller transition has succeeded only if the animator completes at its .end position. Any other case means the transition has been canceled, so you can call completeTransition(false) directly. Build and run and try wiggling the table up and down a bit - sweet!
But there’s a problem. Think for a second about your non-interactive transition. Tap on Edit. Something is wrong!
You need to change your code to explicitly set the view controller transition to non-interactive whenever the user taps the button.
Switch back to LockScreenViewController.swift and find the widgets table data source method tableView(_:cellForRowAt:).
You will see that the code assigns a closure to the Edit button, like so:
(cell as? FooterCell)?.didPressEdit = { [unowned self] in
self.presentSettings()
}
Just before the self.presentSettings() line, insert:
self.presentTransition.wantsInteractiveStart = false
This ensures that you are presenting the settings view controller non-interactively. Run the app another time and give the transition a try.
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.
In this part of the chapter, you will add code to start presenting the settings controller after a tap on Edit, but pause the transition if the user taps again on the screen during the animation.
Switch to PresentTranstion.swift. To allow touches during a non-interactive transition, you have to explicitly set the animator as able to handle user activity. In transitionAnimator(using:), insert this line towards the bottom:
animator.isUserInteractionEnabled = true
You make sure the transition animation is interactive so that the user can continue interacting with the screen after they’ve paused it.
You will allow the user to scroll either up or down to complete or cancel the transition respectively. To do that, switch back to LockScreenViewController.swift and add a new property:
var touchesStartPointY: CGFloat?
In case the user touches the screen during a transition, you pause it and store the location of that first touch:
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()
}
You check if the touch happened during a non-interactive transition and the transition’s animator is currently running.
In that case, you store the current touch location and then call pause() on the transition, which will pause the transition and leave it in an interactive mode.
Run the app, tap on Edit, and quickly tap again a second time. The transition will freeze onscreen like so:
Now you need to track the user touches and see if the user pans up or down. Add the following:
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()
}
}
With this rather big chunk of code, your non-interactive-turned-interactive transition is complete!
You observe for two different cases. First, if the user moves their touch downwards more than 40 points, you cancel the transition and reset the blur effect. If the user moved their touch upwards more than 40 points, you complete the transition successfully. Give the app a try one last time. Tap on Edit, tap again to pause the transition, and either cancel or complete it depending on the direction you pan. And that’s all for this section of the book!
You’ve learned plenty about UIViewPropertyAnimator and how to make the best of it. You’ve worked through a rather lengthy four chapters but you achieved a lot, and the project looks amazing:
Key Points
- By combining your knowledge about custom transitions and creating interactive, interruptible animations with
UIViewPropertyAnimator, you can create stunning transition effects.