In iOS view transitions can be interactive and non-interactive. In this post we are going to take a look on how to implement a custom non-interactive transition.
Setting up a custom transition
For setting up a custom non-interactive transition it is needed to create an animator object defining the transition and feeding it into UIKit. Before view controller is presented, we’ll need to change the UIModalPresentationStyle to custom, set delegate and with delegate method providing the custom animator to UIKit.
final class ViewController: UIViewController, UIViewControllerTransitioningDelegate { | |
@objc func showView() { | |
let presentedViewController = PresentedViewController() | |
presentedViewController.modalPresentationStyle = .custom | |
presentedViewController.transitioningDelegate = self | |
present(presentedViewController, animated: true, completion: nil) | |
} | |
private let transition = CustomTransition() | |
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { | |
return transition | |
} | |
} |
Custom animator
Custom animator object needs to conform to UIViewControllerContextTransitioning. It is required to implement a method defining the duration of the transition and method performing the transition. UIKit calls those methods and provides a UIViewControllerContextTransitioning object what gives contextual information about the transition (e.g. view controllers related to the transition). It is important to check isAnimated
property for seeing if the transition should be animated at all. Secondly, it is required to call completeTransition()
when transition has finished.
Let’s take a look on an example implementation of custom transition. In this particular case Core Animation is used for implementing animations. Several animations run in an animation group, and when it finishes, completeTransition()
is called. Core Animation is used because of the need to rotate the presented view which is easy to do with CABasicAnimation
. Just for keeping in mind that most of the simpler animations might be easier just to implement with UIView’s animate(withDuration:delay:options:animations:completion:)
.
final class CustomTransition: NSObject, CAAnimationDelegate, UIViewControllerAnimatedTransitioning { | |
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { | |
return 1.0 | |
} | |
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { | |
guard let toViewController = transitionContext.viewController(forKey: .to) else { return } | |
transitionContext.containerView.addSubview(toViewController.view) | |
if transitionContext.isAnimated { | |
toViewController.view.frame = transitionContext.finalFrame(for: toViewController) | |
let opacity: CABasicAnimation = { | |
let animation = CABasicAnimation(keyPath: "opacity") | |
animation.duration = transitionDuration(using: transitionContext) | |
animation.fromValue = 0.0 | |
animation.timingFunction = CAMediaTimingFunction(name: .easeIn) | |
animation.toValue = 1.0 | |
return animation | |
}() | |
let rotation: CABasicAnimation = { | |
let animation = CABasicAnimation(keyPath: "transform.rotation") | |
animation.duration = transitionDuration(using: transitionContext) | |
animation.fromValue = 0.0 | |
animation.toValue = 2.0 * 2.0 * Double.pi | |
animation.timingFunction = CAMediaTimingFunction(name: .easeIn) | |
return animation | |
}() | |
let scale: CABasicAnimation = { | |
let animation = CABasicAnimation(keyPath: "transform.scale") | |
animation.duration = transitionDuration(using: transitionContext) | |
animation.fromValue = 0.1 | |
animation.toValue = 1.0 | |
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) | |
return animation | |
}() | |
let group: CAAnimationGroup = { | |
let group = CAAnimationGroup() | |
group.animations = [opacity, rotation, scale] | |
group.delegate = self | |
group.duration = transitionDuration(using: transitionContext) | |
return group | |
}() | |
self.transitionContext = transitionContext | |
toViewController.view.layer.add(group, forKey: "rotateScaleGroup") | |
} | |
else { | |
toViewController.view.frame = transitionContext.finalFrame(for: toViewController) | |
transitionContext.completeTransition(true) | |
} | |
} | |
private var transitionContext: UIViewControllerContextTransitioning? = nil | |
func animationDidStop(_ animation: CAAnimation, finished isFinished: Bool) { | |
transitionContext?.completeTransition(isFinished) | |
transitionContext = nil | |
} | |
} |
Here is the end result.
Summary
In this blog post we took a look on how to use custom transitions when presenting a view controller. It was a matter of setting presentation style to custom and creating and providing an animator object to UIKit using a delegate.
Playground
CustomViewTransition (GitHub) Xcode 10, Swift 4.2