108

I'm switching tabs programmatically in a tab bar driven application using UITabBarController.selectedIndex. The problem I'm trying to solve is how to animate the transition between the views. ie. from the view of the current tab to the view of the selected tab.

The first thought was to make use of the UITabBarControllerDelegate, but it appears that this is not called when programmatically switching tabs. I'm now considering the UITabBarDelegate.didSelectItem: as a possible hook to set a transition animation.

Has anyone managed to animate the transitions? If yes, how ?

alxx
  • 9,882
  • 4
  • 24
  • 40
drekka
  • 18,723
  • 12
  • 67
  • 112
  • 1
    FWIW, many of these highly voted answers predate custom transitions outlined in [Runo's answer](https://stackoverflow.com/a/32064721/1271826) and [Heberti's, too](https://stackoverflow.com/a/32120593/1271826). Those are the right way to tackle these custom animations. See WWDC 2013 video [Custom Transitions Using View Controllers](https://developer.apple.com/videos/play/wwdc2013/218/). – Rob Aug 20 '17 at 21:02

19 Answers19

157

Update 04/2016: Justed wanted to update this to say thank you to everyone for all the votes. Please also note that this was originally written way back when ... before ARC, before constraints, before ... a lot of stuff! So please take this into account when deciding whether to use these techniques. There may be more modern approaches. Oh, and if you find one. Please add a response so everyone can see. Thanks.

Some time later ...

After much research I came up with two working solutions. Both of these worked and did the animation between tabs.

Solution 1: transition from view (simple)

This is the easiest and makes use of a predefined UIView transition method. With this solution we don't need to manage the views because the method does the work for us.

// Get views. controllerIndex is passed in as the controller we want to go to. 
UIView * fromView = tabBarController.selectedViewController.view;
UIView * toView = [[tabBarController.viewControllers objectAtIndex:controllerIndex] view];

// Transition using a page curl.
[UIView transitionFromView:fromView 
                    toView:toView 
                  duration:0.5 
                   options:(controllerIndex > tabBarController.selectedIndex ? UIViewAnimationOptionTransitionCurlUp : UIViewAnimationOptionTransitionCurlDown)
                completion:^(BOOL finished) {
                    if (finished) {
                        tabBarController.selectedIndex = controllerIndex;
                    }
                }];

Solution 2: scroll (more complex)

A more complex solution, but gives you more control of the animation. In this example we get the views to slide on and off. With this one we need to manage the views ourselves.

// Get the views.
UIView * fromView = tabBarController.selectedViewController.view;
UIView * toView = [[tabBarController.viewControllers objectAtIndex:controllerIndex] view];

// Get the size of the view area.
CGRect viewSize = fromView.frame;
BOOL scrollRight = controllerIndex > tabBarController.selectedIndex;

// Add the to view to the tab bar view.
[fromView.superview addSubview:toView];

// Position it off screen.
toView.frame = CGRectMake((scrollRight ? 320 : -320), viewSize.origin.y, 320, viewSize.size.height);

[UIView animateWithDuration:0.3 
                 animations: ^{

                     // Animate the views on and off the screen. This will appear to slide.
                     fromView.frame =CGRectMake((scrollRight ? -320 : 320), viewSize.origin.y, 320, viewSize.size.height);
                     toView.frame =CGRectMake(0, viewSize.origin.y, 320, viewSize.size.height);
                 }

                 completion:^(BOOL finished) {
                     if (finished) {

                         // Remove the old view from the tabbar view.
                         [fromView removeFromSuperview];
                         tabBarController.selectedIndex = controllerIndex;                
                     }
                 }];

This Solution in Swift:

extension TabViewController: UITabBarControllerDelegate {
      public func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {

           let fromView: UIView = tabBarController.selectedViewController!.view
           let toView  : UIView = viewController.view
           if fromView == toView {
                 return false
           }

           UIView.transitionFromView(fromView, toView: toView, duration: 0.3, options: UIViewAnimationOptions.TransitionCrossDissolve) { (finished:Bool) in

        }
        return true
   }
}
Bruno
  • 1,523
  • 12
  • 14
drekka
  • 18,723
  • 12
  • 67
  • 112
  • 1
    Thanks very very much for the answer, it works really well. However I found one bug in both solutions, I'm not sure if this happens to everyone, but it seems that when the page is transitioned, there is a gap between the navigation bar and the status bar, then after the animation finishes, the gap closes. This makes the ending of the animation a little bit jittery. Do you know why this is happening? – Enrico Susatyo Jun 17 '11 at 04:19
  • Hmm, wasn't happening with my code. That sounds very much like an issue I've seen before where the positioning of the new views frame is not correct in relation to the window and status bar. Try running toe code to swap views without doing a transition and see if it still occurs. – drekka Jun 17 '11 at 04:57
  • Yep, it still occurs without doing a transition. I tried the first method. It might be the positioning of the frame, i'll play around with it a bit more. I tried shifting the frame up, and tried matching the frame with the fromView but have no luck so far... – Enrico Susatyo Jun 17 '11 at 05:12
  • It seems that it's similar to this question: http://stackoverflow.com/questions/2144502/how-come-some-of-my-uiviews-are-shifted-after-navigation But I haven't worked it out yet – Enrico Susatyo Jun 17 '11 at 05:31
  • the method i think is "-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController" but I've another problem: where I get controllerIndex? I used "controllerIndex = [tabBarController.viewControllers indexOfObject:controllerView]" and it's correct, but "tabBarController.selectedIndex" return the same number. It should be the start page instead, right? – Syco Feb 05 '12 at 12:42
  • 2
    @EmileCormier put it in the TabBar delegate's `shouldSelectViewController` method and return NO there – cheesus Jun 20 '12 at 15:08
  • @Syco the second argument of `shouldSelectViewController` is the view of the 'new' Tab.. the index of the 'new' tab cannot be the same as the `tabBarController.selectedIndex`. – cheesus Jun 20 '12 at 15:10
  • 2
    @drekka this is not working for me. can you explain were does the controllerIndex comes from ? and why don't you just use [viewController view] from the tabBarControllerDelegate method for the 'toView'? Thnaks – shannoga Aug 18 '12 at 05:52
  • Hi @shannoga In the examples above I was dealing with a tabbar and switching between the views of the controllers it managed. The to and from view are up to you and your project. I was more trying to show two different ways to handle an animation of a transition. Using the view from the tabBarControllerDelegate may well be an option. I've not needed this code since the project it was written for. :-) – drekka Aug 20 '12 at 00:24
  • Duration should be .4 seconds to emulate the real animation (scrolling) – Daniel Feb 01 '13 at 10:48
  • This is an old answer, but still there's no one facing the problem with `controllerIndex`? If you have many view controllers in tab bar controller, then a `UIMoreNavigationController` will appear. Its `controllerIndex` is always out of bound. Since it is a private class, we can't check it using `[viewController isKindOfClass:]`, therefore we have to check the index: `if (controllerIndex > tabBarController.viewControllers.count - 1) {...}` – kientux Oct 17 '15 at 03:42
  • Also if you select currently selected view controller, it doesn't go back to root view controller like normal. Have to check that: `if (controllerIndex == tabBarController.selectedIndex) { return YES; }` – kientux Oct 17 '15 at 03:49
25

following is my try to use the code form drekka into the delegate(UITabBarControllerDelegate) method

- (BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {

    NSArray *tabViewControllers = tabBarController.viewControllers;
    UIView * fromView = tabBarController.selectedViewController.view;
    UIView * toView = viewController.view;
    if (fromView == toView)
        return false;
    NSUInteger fromIndex = [tabViewControllers indexOfObject:tabBarController.selectedViewController];
    NSUInteger toIndex = [tabViewControllers indexOfObject:viewController];

    [UIView transitionFromView:fromView
                        toView:toView
                      duration:0.3
                       options: toIndex > fromIndex ? UIViewAnimationOptionTransitionFlipFromLeft : UIViewAnimationOptionTransitionFlipFromRight
                    completion:^(BOOL finished) {
                        if (finished) {
                            tabBarController.selectedIndex = toIndex;
                        }
                    }];
    return true;
}
Ryan Wu
  • 5,168
  • 2
  • 32
  • 44
  • 2
    You should return some value according to the method declaration, but this approach works nice +1 – voromax Mar 05 '13 at 20:41
  • 2
    You can set the delegate to your UITabController implementation file by adding self.delegate = self; in your viewDidLoad() function. This will allow the above function to be called. – Chris Fremgen Apr 18 '15 at 04:23
  • If you use this code, keep in mind that the destination view controller will not receive a `viewWillAppear` call until _after_ its view has appeared, when `selectedIndex` is changed in the completion handler. This may cause strange behavior in the destination view controller. You could call `viewWillAppear` before the transition, but then it would get called twice. – Robin Daugherty May 06 '21 at 13:40
21

My solution for iOS7.0 or above.

You can specify a custom animation controller in the tab bar's delegate.

Implement an animation controller like this:

@interface TabSwitchAnimationController : NSObject <UIViewControllerAnimatedTransitioning>

@end

@implementation TabSwitchAnimationController

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
    return 0.2;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController* fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController* toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView* toView = toVC.view;
    UIView* fromView = fromVC.view;

    UIView* containerView = [transitionContext containerView];
    [containerView addSubview:toView];
    toView.frame = [transitionContext finalFrameForViewController:toVC];

    // Animate by fading
    toView.alpha = 0.0;
    [UIView animateWithDuration:[self transitionDuration:transitionContext]
                          delay:0.0
                        options:UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction
                     animations:^{
                         toView.alpha = 1.0;
                     }
                     completion:^(BOOL finished) {
                         toView.alpha = 1.0;
                         [fromView removeFromSuperview];
                         [transitionContext completeTransition:YES];
                     }];
}

@end

Then use it in your UITabBarControllerDelegate:

- (id <UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
            animationControllerForTransitionFromViewController:(UIViewController *)fromVC
                                              toViewController:(UIViewController *)toVC
{
    return [[TabSwitchAnimationController alloc] init];
}
Runo Sahara
  • 695
  • 8
  • 23
  • 2
    And remember to hook up your Delegate to the delegate outlet of the TabViewController. Worked beautifully. The cleanest solution here. – Andrew Duncan Sep 21 '15 at 05:03
  • Can this be done through the storyboard and swift now that I am looking into this feature in IOS 10.x? – mobibob Aug 23 '17 at 23:37
17

Instead of use tabBarController:shouldSelectViewController: is better to implement tabBarController:animationControllerForTransitionFromViewController:toViewController:

TransitioningObject.swift

import UIKit

class TransitioningObject: NSObject, UIViewControllerAnimatedTransitioning {

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        let fromView: UIView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
        let toView: UIView = transitionContext.viewForKey(UITransitionContextToViewKey)!

        transitionContext.containerView().addSubview(fromView)
        transitionContext.containerView().addSubview(toView)

        UIView.transitionFromView(fromView, toView: toView, duration: transitionDuration(transitionContext), options: UIViewAnimationOptions.TransitionCrossDissolve) { finished in
            transitionContext.completeTransition(true)
        }
    }

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
        return 0.25
    }
}

TabBarViewController.swift

import UIKit

    class TabBarViewController: UITabBarController, UITabBarControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.delegate = self
    }

    // MARK: - Tabbar delegate

    func tabBarController(tabBarController: UITabBarController, animationControllerForTransitionFromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return TransitioningObject()
    }
}
Heberti Almeida
  • 1,360
  • 16
  • 27
15

I think you can easily achieve transitions for UITabBarControlelr using CATransition; This will also solve any side effects of using transitionFromView:toView:

Use this inside your custom TabBarController class extended from UITabBarController.

- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController (UIViewController*)viewController {

    CATransition *animation = [CATransition animation];
    [animation setType:kCATransitionFade];
    [animation setDuration:0.25];
    [animation setTimingFunction:[CAMediaTimingFunction functionWithName:
                              kCAMediaTimingFunctionEaseIn]];
    [self.view.window.layer addAnimation:animation forKey:@"fadeTransition"];
}

Hope this helps :)

flopr
  • 450
  • 4
  • 22
13

I wrote a post after trying the various answers here.

The code is in Swift, and you can programatically change the tab with animation by calling animateToTab.

func animateToTab(toIndex: Int) {
    let tabViewControllers = viewControllers!
    let fromView = selectedViewController!.view
    let toView = tabViewControllers[toIndex].view    
    let fromIndex = tabViewControllers.indexOf(selectedViewController!)

    guard fromIndex != toIndex else {return}

    // Add the toView to the tab bar view
    fromView.superview!.addSubview(toView)

    // Position toView off screen (to the left/right of fromView)
    let screenWidth = UIScreen.mainScreen().bounds.size.width;
    let scrollRight = toIndex > fromIndex;
    let offset = (scrollRight ? screenWidth : -screenWidth)
    toView.center = CGPoint(x: fromView.center.x + offset, y: toView.center.y)

    // Disable interaction during animation
    view.userInteractionEnabled = false

    UIView.animateWithDuration(0.5, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {

            // Slide the views by -offset
            fromView.center = CGPoint(x: fromView.center.x - offset, y: fromView.center.y);
            toView.center   = CGPoint(x: toView.center.x - offset, y: toView.center.y);

        }, completion: { finished in

            // Remove the old view from the tabbar view.
            fromView.removeFromSuperview()
            self.selectedIndex = toIndex
            self.view.userInteractionEnabled = true
        })
}

If you want all tab change to have the animation, then hook it to UITabBarControllerDelegate like as such:

func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
    let tabViewControllers = tabBarController.viewControllers!
    guard let toIndex = tabViewControllers.indexOf(viewController) else {
        return false
    }

    // Our method
    animateToTab(toIndex)

    return true
}
samwize
  • 21,403
  • 14
  • 118
  • 171
9

My solution in Swift:

Create custom TabBar class and set it in your storyboard TabBar

class MainTabBarController: UITabBarController, UITabBarControllerDelegate {

override func viewDidLoad() {
    super.viewDidLoad()
    self.delegate = self
    // Do any additional setup after loading the view.
}

func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {


    let tabViewControllers = tabBarController.viewControllers!
    let fromView = tabBarController.selectedViewController!.view
    let toView = viewController.view

    if (fromView == toView) {
        return false
    }

    let fromIndex = tabViewControllers.indexOf(tabBarController.selectedViewController!)
    let toIndex = tabViewControllers.indexOf(viewController)

    let offScreenRight = CGAffineTransformMakeTranslation(toView.frame.width, 0)
    let offScreenLeft = CGAffineTransformMakeTranslation(-toView.frame.width, 0)

    // start the toView to the right of the screen


    if (toIndex < fromIndex) {
        toView.transform = offScreenLeft
        fromView.transform = offScreenRight
    } else {
        toView.transform = offScreenRight
        fromView.transform = offScreenLeft
    }

    fromView.tag = 124
    toView.addSubview(fromView)

    self.view.userInteractionEnabled = false
    UIView.animateWithDuration(0.5, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: {

        toView.transform = CGAffineTransformIdentity

        }, completion: { finished in

            let subViews = toView.subviews
            for subview in subViews{
                if (subview.tag == 124) {
                    subview.removeFromSuperview()
                }
            }
            tabBarController.selectedIndex = toIndex!
            self.view.userInteractionEnabled = true

    })

    return true
 }

}
samwize
  • 21,403
  • 14
  • 118
  • 171
buxik
  • 2,278
  • 20
  • 30
  • this isnt working in ios9 - error returned from the find method ie Downcast from '[UIViewController]?' to '[UIViewController]' only unwraps optionals; did you mean to use '!'? – lozflan Feb 29 '16 at 22:17
  • This was almost good, except I encountered a bug that will not animate (`finished` will be false). I do not know why that happen, but I think it got to do with CA transformation which thinks there is "nothing to animated". I switched to animating with frames, and that worked. – samwize Apr 27 '16 at 07:54
3

I used @Mofumofu's solution and upgraded it to Swift 1.2 and also implemented an up / down animation. Meaning, the new ViewController comes up and pushes the old one up if the new viewcontroller's index is greater than the old one's. Otherwise the direction is down.

class TabScrollPageAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

    let tabBarController: UITabBarController

    init(tabBarController: UITabBarController) {
        self.tabBarController = tabBarController
    }

    func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
        return 0.5
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        if let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey),
            let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) {
                let fromView = fromVC.view
                let toView = toVC.view

                let containerView = transitionContext.containerView()

                var directionUpwardMultiplier: CGFloat = 1.0
                if let vcs = tabBarController.viewControllers as? [UIViewController],
                    let fIndex = find(vcs, fromVC),
                    let tIndex = find(vcs, toVC) {
                        directionUpwardMultiplier = (fIndex < tIndex) ? +1.0 : -1.0
                }

                containerView.clipsToBounds = false
                containerView.addSubview(toView)

                var fromViewEndFrame = fromView.frame
                fromViewEndFrame.origin.y -= (containerView.frame.height * directionUpwardMultiplier)

                let toViewEndFrame = transitionContext.finalFrameForViewController(toVC)
                var toViewStartFrame = toViewEndFrame
                toViewStartFrame.origin.y += (containerView.frame.height * directionUpwardMultiplier)
                toView.frame = toViewStartFrame

                toView.alpha = 0.0
                UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
                    toView.alpha = 1.0
                    toView.frame = toViewEndFrame
                    fromView.alpha = 0.0
                    fromView.frame = fromViewEndFrame
                }, completion: { (completed) -> Void in
                    toView.alpha = 1.0
                    fromView.removeFromSuperview()
                    transitionContext.completeTransition(completed)
                    containerView.clipsToBounds = true
                })

        }
    }

}

In the Container ViewController:

extension XYViewController: UITabBarControllerDelegate {

    func tabBarController(tabBarController: UITabBarController, animationControllerForTransitionFromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return TabScrollPageAnimationController(tabBarController: tabBarController)
    }

}
Kádi
  • 2,586
  • 14
  • 22
3

Here is my Swift 3 solution:

I override selectedIndex of my UITabBarViewController like this:

override var selectedIndex: Int{
    get{
        return super.selectedIndex
    }
    set{
        animateToTab(toIndex: newValue)
        super.selectedIndex = newValue
    }
}

Then I use this function that mimics native push/pop animation:

func animateToTab(toIndex: Int) {
    guard let tabViewControllers = viewControllers, tabViewControllers.count > toIndex, let fromViewController = selectedViewController, let fromIndex = tabViewControllers.index(of: fromViewController), fromIndex != toIndex else {return}

    view.isUserInteractionEnabled = false

    let toViewController = tabViewControllers[toIndex]
    let push = toIndex > fromIndex
    let bounds = UIScreen.main.bounds

    let offScreenCenter = CGPoint(x: fromViewController.view.center.x + bounds.width, y: toViewController.view.center.y)
    let partiallyOffCenter = CGPoint(x: fromViewController.view.center.x - bounds.width*0.25, y: fromViewController.view.center.y)

    if push{
        fromViewController.view.superview?.addSubview(toViewController.view)
        toViewController.view.center = offScreenCenter
    }else{
        fromViewController.view.superview?.insertSubview(toViewController.view, belowSubview: fromViewController.view)
        toViewController.view.center = partiallyOffCenter
    }

    UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseIn, animations: {
        toViewController.view.center   = fromViewController.view.center
        fromViewController.view.center = push ? partiallyOffCenter : offScreenCenter
    }, completion: { finished in
        fromViewController.view.removeFromSuperview()
        self.view.isUserInteractionEnabled = true
    })
}

I hope it helps :)

Nyfa117
  • 256
  • 1
  • 6
2

a fix for the jumpy animation...

UIView * fromView = self.view.superview;

texian
  • 347
  • 4
  • 13
2

this can be solved in two ways

1 - Write this in your AppDelegate.m file once. Remember to include UITabBarControllerDelegate using <> after colon (:) in your AppDelegate.h

-(void)tabBarController:(UITabBarController *)tabBarControllerThis didSelectViewController:(UIViewController *)viewController
{
    [UIView transitionWithView:viewController.view
                      duration:0.1
                       options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionTransitionCrossDissolve
                    animations:^(void){
                    } completion:^(BOOL finished){
                        [UIView beginAnimations:@"animation" context:nil];
                        [UIView setAnimationDuration:0.7];
                        [UIView setAnimationBeginsFromCurrentState:YES];
                        [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft
                                               forView:viewController.view
                                                 cache:NO];
                        [UIView commitAnimations];
                    }];
}

2 - Write this in each of your ViewController.m file

-(void)viewWillAppear:(BOOL)animated
{
    [UIView transitionWithView:self.view
                      duration:1.0
                       options:UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionTransitionCrossDissolve
                    animations:^(void){
                        [super viewWillAppear:YES];
                    } completion:^(BOOL finished){
                    }];
}

hope this help...!

eagle
  • 396
  • 1
  • 5
  • 18
  • 1
    How can I animate transitions between Navigation Controllers? The tabBarControllerDelegate only works with View Controllers. – saeppi Jun 02 '13 at 10:39
  • I tried both, the first one displayed the new view and than animated it which looked odd. The 2nd one didn't seem to have any affect. I went into the view associated with tab2 and added the code to the viewWillAppear and tested it and there was no visable animation between tabs. – Shannon Cole Jun 13 '13 at 02:44
  • Tried this one with default Xcode TabBarController project. No luck with either 1 or 2. I really wanted them to work. :) Am I just missing something? – Andrew Duncan Sep 21 '15 at 04:12
  • Me too , no luck .. any ideas? – Jon Oct 30 '15 at 20:15
2

You can animate depending on the tapped Item - In this example we flipFromLeft if the tapped index is > than the previous selected index and we flipFromRight if the tapped index is < than the previous selected index. This is Swift 4: Implement the UITabBarControllerDelegate method

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {

    let fromView: UIView = tabBarController.selectedViewController!.view
    let toView: UIView = viewController.view

    if fromView == toView {
        return false
    }

    if let tappedIndex = tabBarController.viewControllers?.index(of: viewController) {
        if tappedIndex > tabBarController.selectedIndex {
            UIView.transition(from: fromView, to: toView, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromLeft, completion: nil)
        } else {
            UIView.transition(from: fromView, to: toView, duration: 0.5, options: UIViewAnimationOptions.transitionFlipFromRight, completion: nil)
        }
    }
    return true
}
Teetz
  • 2,647
  • 2
  • 19
  • 28
  • this is not working. I've implemented it in view controller – devedv Jun 21 '18 at 09:43
  • @devedv what is not working with this solution? Did you set the UITabBarControllerDelegate to your ViewController? – Teetz Jun 21 '18 at 10:18
  • yes i did the following in AppDelegate class AppDelegate: UIResponder, UIApplicationDelegate, UITabBarControllerDelegate {}. I'm new to swift can you elaborate the steps in your answer pls? – devedv Jun 21 '18 at 10:20
  • @devdev If this is your AppDelegate class put the function above in your AppDelegate and should be working – Teetz Jun 21 '18 at 10:22
  • I tried but unfortunately, it is not.@teetz check the two view controllers and AppDelegate File here. codeshare.io/GAnL4r – devedv Jun 21 '18 at 10:34
  • Im using swift 4.1 @Teetz – devedv Jun 21 '18 at 10:49
  • @devdev What is the exact problem? Is the tab switching but not animated? Or what issue are you facing? I am using this code and it is working as expected – Teetz Jun 21 '18 at 12:28
  • 1
    Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/173559/discussion-between-teetz-and-devedv). – Teetz Jun 21 '18 at 12:29
  • u r amazing. Thanks! @Teetz – devedv Jun 21 '18 at 13:17
1

drekka's answer is really great. I tweaked the scroll transition a bit so the animation looked more like Apple's push animation. I added an additional animation upon the completion of the first animation to get that sliding effect to look right.

// Disable interaction during animation to avoids bugs.
self.tabBarController.view.userInteractionEnabled = NO;

// Get the views.
UIView * fromView = tabBarController.selectedViewController.view;
UIView * toView = [[tabBarController.viewControllers objectAtIndex:controllerIndex] view];

// Get the size of the view area.
CGRect viewSize = fromView.frame;
BOOL scrollRight = controllerIndex > tabBarController.selectedIndex;

// Add the to view to the tab bar view.
[fromView.superview addSubview:toView];
[fromView.superview addSubview:fromView];

self.tabBarController.selectedIndex = 0;

// Position it off screen.
toView.frame = CGRectMake((scrollRight ? (viewSize.size.width *.25) : -(viewSize.size.width * .25 )), viewSize.origin.y, viewSize.size.width, viewSize.size.height);

[UIView animateWithDuration:0.25 
             animations: ^{
                 // Animate the views on and off the screen.
                 [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
                 fromView.frame = CGRectMake(viewSize.size.width * .95, viewSize.origin.y, viewSize.size.width, viewSize.size.height);
                 toView.frame = CGRectMake((viewSize.origin.x * .90), viewSize.origin.y, viewSize.size.width, viewSize.size.height);
             }

             completion:^(BOOL finished) {
                 if (finished) {
                     // Being new animation.
                     [UIView animateWithDuration:0.2
                                          animations: ^{
                                              [UIView setAnimationCurve:UIViewAnimationCurveLinear];
                                              fromView.frame = CGRectMake(viewSize.size.width, viewSize.origin.y, viewSize.size.width, viewSize.size.height);
                                              toView.frame = CGRectMake((viewSize.origin.x), viewSize.origin.y, viewSize.size.width, viewSize.size.height);
                                          }
                                          completion:^(BOOL finished) {
                                              if (finished) {
                                                  // Remove the old view from the tabbar view.
                                                  [fromView removeFromSuperview];
                                                  // Restore interaction.
                                                  self.tabBarController.view.userInteractionEnabled = YES;
                                              }
                                          }];
                 }
             }];
Terry Torres
  • 101
  • 1
  • 7
0

I wanted to use a flip transition between two child view controllers on a button press and achieved it as follows:

-(IBAction)flipViewControllers:(id)sender{
    NSUInteger index = self.selectedIndex;
    index++;
    if(index >= self.childViewControllers.count){
        index = 0;
    }

    self.selectedIndex = index;

    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.75];
    [UIView setAnimationTransition:index % 2 ? UIViewAnimationTransitionFlipFromLeft : UIViewAnimationTransitionFlipFromRight
                           forView:self.view
                             cache:YES];
    [UIView commitAnimations];
}

I also set the background colour to black, in my case I did that by setting the navigationController.view.backgroundColor but in your case it might be the window.backgroundColor which can easily be set in the app delegate.

malhal
  • 17,500
  • 6
  • 94
  • 112
0

Here's my working code (for 3 tabs, haven't tried it on more!!) to animate transitions between tabs. It's mainly based on drekka's solution, but already implemented in the tabbar's delegate method, so it should do the job if you just copy/paste it.. (you never know!)

-(BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController {

// Important! We validate that the selected tab is not the current tab, to avoid misplacing views
if (tabBarController.selectedViewController == viewController) {
    return NO;
}

// Find the selected view's index
NSUInteger controllerIndex = 0;
for (UIViewController *vc in tabBarController.viewControllers) {
    if (vc == viewController) {
        controllerIndex = [tabBarController.viewControllers indexOfObject:vc];
    }
}

CGFloat screenWidth = SCREEN_SIZE.width;

// Note: We must invert the views according to the direction of the scrolling ( FROM Left TO right or FROM right TO left )
UIView * fromView = tabBarController.selectedViewController.view;
UIView * toView = viewController.view;

[fromView.superview addSubview:toView];
CGRect fromViewInitialFrame = fromView.frame;
CGRect fromViewNewframe = fromView.frame;

CGRect toViewInitialFrame = toView.frame;

if ( controllerIndex > tabBarController.selectedIndex ) {
// FROM left TO right ( tab0 to tab1 or tab2 )

    // The final frame for the current view. It will be displaced to the left
    fromViewNewframe.origin.x = -screenWidth;
    // The initial frame for the new view. It will be displaced to the left
    toViewInitialFrame.origin.x = screenWidth;
    toView.frame = toViewInitialFrame;

} else {
// FROM right TO left ( tab2 to tab1 or tab0 )

    // The final frame for the current view. It will be displaced to the right
    fromViewNewframe.origin.x = screenWidth;
    // The initial frame for the new view. It will be displaced to the right
    toViewInitialFrame.origin.x = -screenWidth;
    toView.frame = toViewInitialFrame;
}

[UIView animateWithDuration:0.2 animations:^{
    // The new view will be placed where the initial view was placed
    toView.frame = fromViewInitialFrame;
    // The initial view will be place outside the screen bounds
    fromView.frame = fromViewNewframe;

    tabBarController.selectedIndex = controllerIndex;

    // To prevent user interaction during the animation
    [[UIApplication sharedApplication] beginIgnoringInteractionEvents];

} completion:^(BOOL finished) {

    // Before removing the initial view, we adjust its frame to avoid visual lags
    fromView.frame = CGRectMake(0, 0, fromView.frame.size.width, fromView.frame.size.height);
    [fromView removeFromSuperview];

    [[UIApplication sharedApplication] endIgnoringInteractionEvents];
}];

return NO;

}

Nahuel Roldan
  • 573
  • 7
  • 12
  • While this code snippet may solve the question, [including an explanation](http://meta.stackexchange.com/questions/114762/explaining-entirely-code-based-answers) really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. – Ferrybig Feb 26 '16 at 17:36
  • Thanks for the tip Ferrybig! I did try to document the code as much as posible to make it easier to undestand it, hope it helps – Nahuel Roldan Feb 26 '16 at 18:53
0

This works for me in Swift 3:

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {

    if let fromView = tabBarController.selectedViewController?.view, let toView = viewController.view {

        if fromView == toView {
            return false
        }

        UIView.transition(from: fromView, to: toView, duration: 0.2, options: .transitionCrossDissolve) { (finished) in
        }
    }

    return true
}
Thread Pitt
  • 556
  • 1
  • 4
  • 16
0

@samwize Answer translated to Swift 3- 2 thumbs up on this one, creates a left to wright page effect:

func animateToTab(toIndex: Int) {
        let tabViewControllers = viewControllers!
        let fromView = selectedViewController!.view
        let toView = tabViewControllers[toIndex].view
        let fromIndex = tabViewControllers.index(of: selectedViewController!)

        guard fromIndex != toIndex else {return}

        // Add the toView to the tab bar view
        fromView?.superview!.addSubview(toView!)

        // Position toView off screen (to the left/right of fromView)
        let screenWidth = screenSize.width
        let scrollRight = toIndex > fromIndex!
        let offset = (scrollRight ? screenWidth : -screenWidth)
        toView?.center = CGPoint(x: (fromView?.center.x)! + offset, y: (toView?.center.y)!)

        // Disable interaction during animation
        view.isUserInteractionEnabled = false

        UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {

            // Slide the views by -offset
            fromView?.center = CGPoint(x: (fromView?.center.x)! - offset, y: (fromView?.center.y)!);
            toView?.center   = CGPoint(x: (toView?.center.x)! - offset, y: (toView?.center.y)!);

        }, completion: { finished in

            // Remove the old view from the tabbar view.
            fromView?.removeFromSuperview()
            self.selectedIndex = toIndex
            self.view.isUserInteractionEnabled = true
        })
    }
skyguy
  • 6,503
  • 11
  • 53
  • 137
0

@samwize's answer updated for Swift 5:

If you want all tab changes to have the animation, then use a UITabBarControllerDelegate and implement this method:

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
  let tabViewControllers = tabBarController.viewControllers!
  guard let toIndex = tabViewControllers.indexOf(value:viewController) else {
    return false
  }
  animateToTab(toIndex: toIndex, fadeOutFromView: false, fadeInToView: false)
  return true
}

Programmatically change the tab with animation by calling animateToTab:

func animateToTab(toIndex: Int, fadeOutFromView: Bool, fadeInToView: Bool) {
  let tabViewControllers = viewControllers!
  let fromView = selectedViewController!.view
  let toView = tabViewControllers[toIndex].view
  let fromIndex = tabViewControllers.indexOf(value:selectedViewController!)
  guard fromIndex != toIndex else {return}

  // Add the toView to the tab bar view
  fromView!.superview!.addSubview(toView!)

  // Position toView off screen (to the left/right of fromView)
  let screenWidth = UIScreen.main.bounds.width
  let scrollRight = toIndex > fromIndex!;
  let offset = (scrollRight ? screenWidth : -screenWidth)
  toView!.center = CGPoint(x: fromView!.center.x + offset, y: toView!.center.y)

  // Disable interaction during animation
  view.isUserInteractionEnabled = false
  if fadeInToView {
    toView!.alpha = 0.1
  }

  UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.curveEaseOut], animations: {

    if fadeOutFromView {
      fromView!.alpha = 0.0
    }

    if fadeInToView {
      toView!.alpha = 1.0
    }

    // Slide the views by -offset
    fromView!.center = CGPoint(x: fromView!.center.x - offset, y: fromView!.center.y);
    toView!.center   = CGPoint(x: toView!.center.x - offset, y: toView!.center.y);

  }, completion: { finished in
    // Remove the old view from the tabbar view.
    fromView!.removeFromSuperview()
    self.selectedIndex = toIndex
    self.view.isUserInteractionEnabled = true
  })
}
spnkr
  • 462
  • 5
  • 12
-2

Swift 4+

You UITabBarControllerDelegate method should be like this,

func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {

    animateToTab(toIndex: (tabBarController.viewControllers?.index(of: viewController))!)
    return true
}

And the method is,

func animateToTab(toIndex: Int) {
    let tabViewControllers = viewControllers!
    let fromView = selectedViewController!.view
    let toView = tabViewControllers[toIndex].view
    let fromIndex = tabViewControllers.index(of: selectedViewController!)

    guard fromIndex != toIndex else {return}

    // Add the toView to the tab bar view
    fromView!.superview!.addSubview(toView!)

    // Position toView off screen (to the left/right of fromView)
    let screenWidth = UIScreen.main.bounds.size.width;
    let scrollRight = toIndex > fromIndex!;
    let offset = (scrollRight ? screenWidth : -screenWidth)
    toView!.center = CGPoint(x: fromView!.center.x + offset, y: toView!.center.y)

    // Disable interaction during animation
    view.isUserInteractionEnabled = false

    UIView.animate(withDuration: 0.5, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {

        // Slide the views by -offset
        fromView!.center = CGPoint(x: fromView!.center.x - offset, y: fromView!.center.y);
        toView!.center   = CGPoint(x: toView!.center.x - offset, y: toView!.center.y);

    }, completion: { finished in

        // Remove the old view from the tabbar view.
        fromView!.removeFromSuperview()
        self.selectedIndex = toIndex
        self.view.isUserInteractionEnabled = true
    });

}
Zumry Mohamed
  • 7,994
  • 5
  • 40
  • 48