Recently I have started working on a new personal project where I had a static background for the entire application and all my view controllers’ views had a transparent background.
Default transition animation overlaps a little bit the source and destination views, therefore, there is only one way to make our application navigate with some dignity and smoothness — that’s right — it is custom animated transitioning and since we usually want to keep swipe to back gesture on view controllers we also have to create custom interactive transitioning as well.
I have created a sample project — you can find it on GitHub.
The first thing we have to do is implement custom transition for our UINavigationController
and implement its delegate method
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {}
which provides an object that is responsible for providing information on how our custom transition will animate source and destination views while navigating between them.
To add user interaction to a view controller transition we have to implement another method (also will be needed to handle back gesture properly) is
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {}
Both methods are depending on each other while creating custom, interactive transition and are available since iOS 7.0 so pretty much all new or updated applications can get benefits from them.
First of all, we have to create a new object that will be implementing UIViewControllerAnimatedTransitioning
protocol.
final class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
}
Since mentioned protocol conforms to NSObjectProtocol
our animator object have to conform to it as well and the easier way to achieve that is to simply inherit from NSObject
. UIViewControllerAnimatedTransitioning
requires two methods which you can see above as well as there are two optional methods which we will not need in our project today so let’s not get interrupted by them.
Our custom animator object needs to know if the transition is invoked by a push (being presented) or a pop (being dismissed) action of navigation controller so we will add Bool
property which will store that information. Since we will create a quite simple custom animation we will not need any additional properties — if you want to create more complex animation you should store here everything that will be needed for your animation (ie. you could store here frame of a tapped button so you can animate present another controller from that point).
As an example, I have created a simple “fade and slide/move” animation. I will explain each part of my TransitionAnimator below.
final class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
// 1
let presenting: Bool
// 2
init(presenting: Bool) {
self.presenting = presenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
// 3
return TimeInterval(UINavigationControllerHideShowBarDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// 4
guard let fromView = transitionContext.view(forKey: .from) else { return }
guard let toView = transitionContext.view(forKey: .to) else { return }
// 5
let duration = transitionDuration(using: transitionContext)
// 6
let container = transitionContext.containerView
if presenting {
container.addSubview(toView)
} else {
container.insertSubview(toView, belowSubview: fromView)
}
// 7
let toViewFrame = toView.frame
toView.frame = CGRect(x: presenting ? toView.frame.width : -toView.frame.width, y: toView.frame.origin.y, width: toView.frame.width, height: toView.frame.height)
let animations = {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.5) {
toView.alpha = 1
if self.presenting {
fromView.alpha = 0
}
}
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1) {
toView.frame = toViewFrame
fromView.frame = CGRect(x: self.presenting ? -fromView.frame.width : fromView.frame.width, y: fromView.frame.origin.y, width: fromView.frame.width, height: fromView.frame.height)
if !self.presenting {
fromView.alpha = 0
}
}
}
UIView.animateKeyframes(withDuration: duration,
delay: 0,
options: .calculationModeCubic,
animations: animations,
completion: { finished in
// 8
container.addSubview(toView)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
UINavigationController
to fit other (hiding/showing bar) animations in the application.UIViewControllerContextTransitioning’s
method view(forKey: )
. If needed there is also an option to access the whole view controller, just use viewController(forKey: )
instead.Wow, that was pretty easy, wasn’t it? 😎 Now it is time to attach our custom transition to our navigation controller — how to do that? Well, there is few possible ways to go but we want to stay Swifty! To achieve that let’s choose a little bit harder (is it?) path and instead of inheriting from either UINavigationController
or UIViewController
lets create TransitionCoordinator
(not sure if the name fits here but that is how I called it for now, if you have any suggestions, leave it in comments below). Our coordinator should conform to UINavigationControllerDelegate
protocol (Hello NSObject
again).
final class TransitionCoordinator: NSObject, UINavigationControllerDelegate {
// 1
var interactionController: UIPercentDrivenInteractiveTransition?
// 2
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return TransitionAnimator(presenting: true)
case .pop:
return TransitionAnimator(presenting: false)
default:
return nil
}
}
// 3
func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return interactionController
}
}
interactionController
to handle interactive transitioning.Finally, the fun begins! As I mentioned we do not want to inherit from anything and we want to be able to easily attach our custom transition to any UINavigationController
we want, to do that let’s create an extension.
extension UINavigationController {
// 1
static private var coordinatorHelperKey = "UINavigationController.TransitionCoordinatorHelper"
// 2
var transitionCoordinatorHelper: TransitionCoordinator? {
return objc_getAssociatedObject(self, &UINavigationController.coordinatorHelperKey) as? TransitionCoordinator
}
func addCustomTransitioning() {
// 3
var object = objc_getAssociatedObject(self, &UINavigationController.coordinatorHelperKey)
guard object == nil else {
return
}
object = TransitionCoordinator()
let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
objc_setAssociatedObject(self, &UINavigationController.coordinatorHelperKey, object, nonatomic)
// 4
delegate = object as? TransitionCoordinator
// 5
let edgeSwipeGestureRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipe(_:)))
edgeSwipeGestureRecognizer.edges = .left
view.addGestureRecognizer(edgeSwipeGestureRecognizer)
}
// 6
@objc func handleSwipe(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
guard let gestureRecognizerView = gestureRecognizer.view else {
transitionCoordinatorHelper?.interactionController = nil
return
}
let percent = gestureRecognizer.translation(in: gestureRecognizerView).x / gestureRecognizerView.bounds.size.width
if gestureRecognizer.state == .began {
transitionCoordinatorHelper?.interactionController = UIPercentDrivenInteractiveTransition()
popViewController(animated: true)
} else if gestureRecognizer.state == .changed {
transitionCoordinatorHelper?.interactionController?.update(percent)
} else if gestureRecognizer.state == .ended {
if percent > 0.5 && gestureRecognizer.state != .cancelled {
transitionCoordinatorHelper?.interactionController?.finish()
} else {
transitionCoordinatorHelper?.interactionController?.cancel()
}
transitionCoordinatorHelper?.interactionController = nil
}
}
}
TransitionCoordinator
object.TransitionCoordinator
and associate it with the mentioned key.UIScreenEdgePanGestureRecognizer
which will react to the left edge only and add it to the navigation controller’s view.And…
We are done with the hard work. Now to make the magic happen let’s just call our extension’s method on any navigation controller we want (navigationController.addCustomTransitioning()
).