How to animate Tab bar tab switch with a CrossDissolve slide transition?
There is a simpler way to doing this. Add the following code in the tabbar delegate:
Working on Swift 2, 3 and 4
class MySubclassedTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() delegate = self }}extension MySubclassedTabBarController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { guard let fromView = selectedViewController?.view, let toView = viewController.view else { return false // Make sure you want this as false } if fromView != toView { UIView.transition(from: fromView, to: toView, duration: 0.3, options: [.transitionCrossDissolve], completion: nil) } return true }}
EDIT (4/23/18)Since this answer is getting popular, I updated the code to remove the force unwraps, which is a bad practice, and added the guard statement.
EDIT (7/11/18)@AlbertoGarcía is right. If you tap the tabbar icon twice you get a blank screen. So I added an extra check
If you want to use UIViewControllerAnimatedTransitioning
to do something more custom than UIView.transition
, take a look at this gist.
// MyTabController.swiftimport UIKitclass MyTabBarController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() delegate = self }}extension MyTabBarController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return MyTransition(viewControllers: tabBarController.viewControllers) }}class MyTransition: NSObject, UIViewControllerAnimatedTransitioning { let viewControllers: [UIViewController]? let transitionDuration: Double = 1 init(viewControllers: [UIViewController]?) { self.viewControllers = viewControllers } func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return TimeInterval(transitionDuration) } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from), let fromView = fromVC.view, let fromIndex = getIndex(forViewController: fromVC), let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), let toView = toVC.view, let toIndex = getIndex(forViewController: toVC) else { transitionContext.completeTransition(false) return } let frame = transitionContext.initialFrame(for: fromVC) var fromFrameEnd = frame var toFrameStart = frame fromFrameEnd.origin.x = toIndex > fromIndex ? frame.origin.x - frame.width : frame.origin.x + frame.width toFrameStart.origin.x = toIndex > fromIndex ? frame.origin.x + frame.width : frame.origin.x - frame.width toView.frame = toFrameStart DispatchQueue.main.async { transitionContext.containerView.addSubview(toView) UIView.animate(withDuration: self.transitionDuration, animations: { fromView.frame = fromFrameEnd toView.frame = frame }, completion: {success in fromView.removeFromSuperview() transitionContext.completeTransition(success) }) } } func getIndex(forViewController vc: UIViewController) -> Int? { guard let vcs = self.viewControllers else { return nil } for (index, thisVC) in vcs.enumerated() { if thisVC == vc { return index } } return nil }}
I was struggling with the tab bar animation both from a user tap and programmatically calling selectedIndex = X
since the accepted solution didn't work for me when setting the selected tab programatically.
In the end I managed to solve it by a UITabBarControllerDelegate
and a custom UIViewControllerAnimatedTransitioning
as follows:
extension MainController: UITabBarControllerDelegate { public func tabBarController( _ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { return FadePushAnimator() }}
Where the FadePushAnimator looks like this:
class FadePushAnimator: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.3 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let toViewController = transitionContext.viewController(forKey: .to) else { return } transitionContext.containerView.addSubview(toViewController.view) toViewController.view.alpha = 0 let duration = self.transitionDuration(using: transitionContext) UIView.animate(withDuration: duration, animations: { toViewController.view.alpha = 1 }, completion: { _ in transitionContext.completeTransition(!transitionContext.transitionWasCancelled) }) }}
This approach supports any sort of custom animation and works both on user tap and setting the selected tab programatically. Tested on Swift 5.