Question

So I have a button that is connected to a IBAction. When I press the button I want to hide the tab bar in my iOS app with a animation. This [self setTabBarHidden:hidden animated:NO]; or this [self.tabBarController setTabBarHidden:hidden animated:YES]; does not work. This is my code without the animation:

- (IBAction)picture1:(id)sender {
    [self.tabBarController.tabBar setHidden:YES];
}

Any help would be greatly appreciated :D

Was it helpful?

Solution 2

I try to keep view animations available to me using the following formula:

// pass a param to describe the state change, an animated flag and a completion block matching UIView animations completion 
- (void)setTabBarVisible:(BOOL)visible animated:(BOOL)animated completion:(void (^)(BOOL))completion {

    // bail if the current state matches the desired state
    if ([self tabBarIsVisible] == visible) return (completion)? completion(YES) : nil;

    // get a frame calculation ready
    CGRect frame = self.tabBarController.tabBar.frame;
    CGFloat height = frame.size.height;
    CGFloat offsetY = (visible)? -height : height;

    // zero duration means no animation
    CGFloat duration = (animated)? 0.3 : 0.0;

    [UIView animateWithDuration:duration animations:^{
        self.tabBarController.tabBar.frame = CGRectOffset(frame, 0, offsetY);
    } completion:completion];
}

//Getter to know the current state
- (BOOL)tabBarIsVisible {
    return self.tabBarController.tabBar.frame.origin.y < CGRectGetMaxY(self.view.frame);
}

//An illustration of a call to toggle current state
- (IBAction)pressedButton:(id)sender {
    [self setTabBarVisible:![self tabBarIsVisible] animated:YES completion:^(BOOL finished) {
        NSLog(@"finished");
    }];
}

OTHER TIPS

When working with storyboard its easy to setup the View Controller to hide the tabbar on push, on the destination View Controller just select this checkbox:
enter image description here

does not longer work on iOS14, see updated 2nde answer below

Swift 3.0 version, using an extension:

extension UITabBarController {
    
    private struct AssociatedKeys {
        // Declare a global var to produce a unique address as the assoc object handle
        static var orgFrameView:     UInt8 = 0
        static var movedFrameView:   UInt8 = 1
    }
    
    var orgFrameView:CGRect? {
        get { return objc_getAssociatedObject(self, &AssociatedKeys.orgFrameView) as? CGRect }
        set { objc_setAssociatedObject(self, &AssociatedKeys.orgFrameView, newValue, .OBJC_ASSOCIATION_COPY) }
    }
    
    var movedFrameView:CGRect? {
        get { return objc_getAssociatedObject(self, &AssociatedKeys.movedFrameView) as? CGRect }
        set { objc_setAssociatedObject(self, &AssociatedKeys.movedFrameView, newValue, .OBJC_ASSOCIATION_COPY) }
    }
    
    override open func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        if let movedFrameView = movedFrameView {
            view.frame = movedFrameView
        }
    }
    
    func setTabBarVisible(visible:Bool, animated:Bool) {
        //since iOS11 we have to set the background colour to the bar color it seams the navbar seams to get smaller during animation; this visually hides the top empty space...
        view.backgroundColor =  self.tabBar.barTintColor 
        // bail if the current state matches the desired state
        if (tabBarIsVisible() == visible) { return }
        
        //we should show it
        if visible {
            tabBar.isHidden = false
            UIView.animate(withDuration: animated ? 0.3 : 0.0) {
                //restore form or frames
                self.view.frame = self.orgFrameView!
                //errase the stored locations so that...
                self.orgFrameView = nil
                self.movedFrameView = nil
                //...the layoutIfNeeded() does not move them again!
                self.view.layoutIfNeeded()
            }
        }
            //we should hide it
        else {
            //safe org positions
            orgFrameView   = view.frame
            // get a frame calculation ready
            let offsetY = self.tabBar.frame.size.height
            movedFrameView = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height + offsetY)
            //animate
            UIView.animate(withDuration: animated ? 0.3 : 0.0, animations: {
                self.view.frame = self.movedFrameView!
                self.view.layoutIfNeeded()
            }) {
                (_) in
                self.tabBar.isHidden = true
            }
        }
    }
    
    func tabBarIsVisible() ->Bool {
        return orgFrameView == nil
    }
}
  • This is based on the input from Sherwin Zadeh after a few hours of playing around.
  • Instead of moving the tabbar itself it moves the frame of the view, this effectively slides the tabbar nicely out of the bottom of the screen but...
  • ... has the advantage that the content displayed inside the UITabbarcontroller is then also taking the full screen!
  • note its also using the AssociatedObject functionality to attached data to the UIView without subclassing and thus an extension is possible (extensions do not allow stored properties)

enter image description here

As per Apple docs, hidesBottomBarWhenPushed property of UIViewController, a Boolean value, indicating whether the toolbar at the bottom of the screen is hidden when the view controller is pushed on to a navigation controller.

The value of this property on the topmost view controller determines whether the toolbar is visible.

The recommended approach to hide tab bar would as follows

    ViewController *viewController = [[ViewController alloc] init];
    viewController.hidesBottomBarWhenPushed = YES;  // This property needs to be set before pushing viewController to the navigationController's stack. 
    [self.navigationController pushViewController:viewController animated:YES];

However, note this approach will only be applied to respective viewController and will not be propagated to other view controllers unless you start setting the same hidesBottomBarWhenPushed property in other viewControllers before pushing it to the navigation controller's stack.

Swift Version:

@IBAction func tap(sender: AnyObject) {
    setTabBarVisible(!tabBarIsVisible(), animated: true, completion: {_ in })
}


// pass a param to describe the state change, an animated flag and a completion block matching UIView animations completion
func setTabBarVisible(visible: Bool, animated: Bool, completion:(Bool)->Void) {

    // bail if the current state matches the desired state
    if (tabBarIsVisible() == visible) {
        return completion(true)
    }

    // get a frame calculation ready
    let height = tabBarController!.tabBar.frame.size.height
    let offsetY = (visible ? -height : height)

    // zero duration means no animation
    let duration = (animated ? 0.3 : 0.0)

    UIView.animateWithDuration(duration, animations: {
        let frame = self.tabBarController!.tabBar.frame
        self.tabBarController!.tabBar.frame = CGRectOffset(frame, 0, offsetY);
    }, completion:completion)
}

func tabBarIsVisible() -> Bool {
    return tabBarController!.tabBar.frame.origin.y < CGRectGetMaxY(view.frame)
}

[Swift4.2]

Just created an extension for UITabBarController:

import UIKit

extension UITabBarController {
    func setTabBarHidden(_ isHidden: Bool, animated: Bool, completion: (() -> Void)? = nil ) {
        if (tabBar.isHidden == isHidden) {
            completion?()
        }

        if !isHidden {
            tabBar.isHidden = false
        }

        let height = tabBar.frame.size.height
        let offsetY = view.frame.height - (isHidden ? 0 : height)
        let duration = (animated ? 0.25 : 0.0)

        let frame = CGRect(origin: CGPoint(x: tabBar.frame.minX, y: offsetY), size: tabBar.frame.size)
        UIView.animate(withDuration: duration, animations: {
            self.tabBar.frame = frame
        }) { _ in
            self.tabBar.isHidden = isHidden
            completion?()
        }
    }
}

For Xcode 11.3 and iOS 13 other answers didn't work for me. However, based on those I've came up to the new solution using CGAffineTransform

I didn't test this code well, but this might actually work.

extension UITabBarController {

    func setTabBarHidden(_ isHidden: Bool) {

        if !isHidden { tabBar.isHidden = false }

        let height = tabBar.frame.size.height
        let offsetY = view.frame.height - (isHidden ? 0 : height)
        tabBar.transform = CGAffineTransform(translationX: 0, y: offsetY)

        UIView.animate(withDuration: 0.25, animations: {
            self.tabBar.transform = .identity
        }) { _ in
            self.tabBar.isHidden = isHidden
        }
    }

}

Hope that helps.

UPDATE 09.03.2020:

I've finally found an awesome implementation of hiding tab bar with animation. It's huge advantage it's able to work either in common cases and in custom navigation controller transitions. Since author's blog is quite unstable, I'll leave the code below. Original source: https://www.iamsim.me/hiding-the-uitabbar-of-a-uitabbarcontroller/

Implementation:

extension UITabBarController {

    /**
     Show or hide the tab bar.

     - Parameter hidden: `true` if the bar should be hidden.
     - Parameter animated: `true` if the action should be animated.
     - Parameter transitionCoordinator: An optional `UIViewControllerTransitionCoordinator` to perform the animation
        along side with. For example during a push on a `UINavigationController`.
     */
    func setTabBar(
        hidden: Bool,
        animated: Bool = true,
        along transitionCoordinator: UIViewControllerTransitionCoordinator? = nil
    ) {
        guard isTabBarHidden != hidden else { return }

        let offsetY = hidden ? tabBar.frame.height : -tabBar.frame.height
        let endFrame = tabBar.frame.offsetBy(dx: 0, dy: offsetY)
        let vc: UIViewController? = viewControllers?[selectedIndex]
        var newInsets: UIEdgeInsets? = vc?.additionalSafeAreaInsets
        let originalInsets = newInsets
        newInsets?.bottom -= offsetY

        /// Helper method for updating child view controller's safe area insets.
        func set(childViewController cvc: UIViewController?, additionalSafeArea: UIEdgeInsets) {
            cvc?.additionalSafeAreaInsets = additionalSafeArea
            cvc?.view.setNeedsLayout()
        }

        // Update safe area insets for the current view controller before the animation takes place when hiding the bar.
        if hidden, let insets = newInsets { set(childViewController: vc, additionalSafeArea: insets) }

        guard animated else {
            tabBar.frame = endFrame
            return
        }

        // Perform animation with coordinato if one is given. Update safe area insets _after_ the animation is complete,
        // if we're showing the tab bar.
        weak var tabBarRef = self.tabBar
        if let tc = transitionCoordinator {
            tc.animateAlongsideTransition(in: self.view, animation: { _ in tabBarRef?.frame = endFrame }) { context in
                if !hidden, let insets = context.isCancelled ? originalInsets : newInsets {
                    set(childViewController: vc, additionalSafeArea: insets)
                }
            }
        } else {
            UIView.animate(withDuration: 0.3, animations: { tabBarRef?.frame = endFrame }) { didFinish in
                if !hidden, didFinish, let insets = newInsets {
                    set(childViewController: vc, additionalSafeArea: insets)
                }
            }
        }
    }

    /// `true` if the tab bar is currently hidden.
    var isTabBarHidden: Bool {
        return !tabBar.frame.intersects(view.frame)
    }

}

If you're dealing with custom navigation transitions just pass a transitionCoordinator property of "from" controller, so animations are in sync:

from.tabBarController?.setTabBar(hidden: true, along: from.transitionCoordinator)

Note, that in such case the initial solution work very glitchy.

Rewrite Sherwin Zadeh's answer in Swift 4:

/* tab bar hide/show animation */
extension AlbumViewController {
    // pass a param to describe the state change, an animated flag and a completion block matching UIView animations completion
    func setTabBarVisible(visible: Bool, animated: Bool, completion: ((Bool)->Void)? = nil ) {

        // bail if the current state matches the desired state
        if (tabBarIsVisible() == visible) {
            if let completion = completion {
                return completion(true)
            }
            else {
                return
            }
        }

        // get a frame calculation ready
        let height = tabBarController!.tabBar.frame.size.height
        let offsetY = (visible ? -height : height)

        // zero duration means no animation
        let duration = (animated ? kFullScreenAnimationTime : 0.0)

        UIView.animate(withDuration: duration, animations: {
            let frame = self.tabBarController!.tabBar.frame
            self.tabBarController!.tabBar.frame = frame.offsetBy(dx: 0, dy: offsetY)
        }, completion:completion)
    }

    func tabBarIsVisible() -> Bool {
        return tabBarController!.tabBar.frame.origin.y < view.frame.maxY
    }
}

I went through the previous posts, so I came out with the solution below as subclass of UITabBarController

Main points are:

  • Written in Swift 5.1
  • Xcode 11.3.1
  • Tested on iOS 13.3
  • Simulated on iPhone 11 and iPhone 8 (so with and without notch)
  • Handles the cases where the user taps on the different tabs
  • Handles the cases where we programmatically change the value of selectedIndex
  • Handles the view controller orientation changes
  • Handles the corner casere where the app moved to background and back to foreground

Below the subclass TabBarController:

class TabBarController: UITabBarController {

    //MARK: Properties
    
    private(set) var isTabVisible:Bool = true
    private var visibleTabBarFrame:CGRect = .zero
    private var hiddenTabBarFrame:CGRect = .zero
    
    override var selectedIndex: Int {
        didSet { self.updateTabBarFrames() }
    }
    
    //MARK: View lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.calculateTabBarFrames()
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: { (_) in }) { (_) in
            // when orientation changes, the tab bar frame changes, so we need to update it to the expected state
            self.calculateTabBarFrames()
            self.updateTabBarFrames()
        }
    }
    
    @objc private func appWillEnterForeground(_ notification:Notification){
        self.updateTabBarFrames()
    }
    
    //MARK: Private
    
    /// Calculates the frames of the tab bar and the expected bounds of the shown view controllers
    private func calculateTabBarFrames() {
        self.visibleTabBarFrame = self.tabBar.frame
        self.hiddenTabBarFrame = CGRect(x: self.visibleTabBarFrame.origin.x, y: self.visibleTabBarFrame.origin.y + self.visibleTabBarFrame.height, width: self.visibleTabBarFrame.width, height: self.visibleTabBarFrame.height)
    }
    
    /// Updates the tab bar and shown view controller frames based on the current expected tab bar visibility
    /// - Parameter tabIndex: if provided, it will update the view frame of the view controller for this tab bar index
    private func updateTabBarFrames(tabIndex:Int? = nil) {
        self.tabBar.frame = self.isTabVisible ? self.visibleTabBarFrame : self.hiddenTabBarFrame
        if let vc = self.viewControllers?[tabIndex ?? self.selectedIndex] {
            vc.additionalSafeAreaInsets.bottom = self.isTabVisible ? 0.0 : -(self.visibleTabBarFrame.height - self.view.safeAreaInsets.bottom)

        }
        self.view.layoutIfNeeded()
    }
    
    //MARK: Public
    
    /// Show/Hide the tab bar
    /// - Parameters:
    ///   - show: whether to show or hide the tab bar
    ///   - animated: whether the show/hide should be animated or not
    func showTabBar(_ show:Bool, animated:Bool = true) {
        guard show != self.isTabVisible else { return }
        self.isTabVisible = show
        guard animated else {
            self.tabBar.alpha = show ? 1.0 : 0.0
            self.updateTabBarFrames()
            return
        }
        UIView.animate(withDuration: 0.25, delay: 0.0, options: [.beginFromCurrentState,.curveEaseInOut], animations: {
            self.tabBar.alpha = show ? 1.0 : 0.0
            self.updateTabBarFrames()
        }) { (_) in }
    }
  
}

extension TabBarController: UITabBarControllerDelegate {
    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        if let tabIndex = self.tabBar.items?.firstIndex(of: item) {
            self.updateTabBarFrames(tabIndex: tabIndex)
        }
    }
}

Sample usage from within a shown view controller:

// hide the tab bar animated (default)
(self.tabBarController as? TabBarController)?.showTabBar(false)
// hide the tab bar without animation
(self.tabBarController as? TabBarController)?.showTabBar(false, animated:false)

Sample output iPhone 11

iPhone 11 sample

Sample output iPhone 8

iPhone 8 sample

EDIT :

  • Updated the code to respect the safe area bottom inset
  • If you're experiencing issues with this solution and your tab bar contains a navigation controller as direct child in the viewControllers array, you may want to make sure that the navigation controller topViewController has the property extendedLayoutIncludesOpaqueBars set to true (you can set this directly from the Storyboard). This should resolve the problem

Hope it helps someone :)

Try to set the frame of the tabBar in animation. See this tutorial.

Just be aware, it's bad practice to do that, you should set show/hide tabBar when UIViewController push by set the property hidesBottomBarWhenPushed to YES.

tried in swift 3.0 / iOS10 / Xcode 8:

    self.tabBarController?.tabBar.isHidden = true

I set it when my controller is shown: (and Hide when back, after navigation)

override func viewWillAppear(_ animated: Bool) {

        super.viewWillAppear(animated)
        self.tabBarController?.tabBar.isHidden = false

    }

    override func viewWillDisappear(_ animated: Bool) {
                super.viewWillDisappear(animated)
        self.tabBarController?.tabBar.isHidden = true

    }

BTW: better to have a flag to save if shown or not, as other vents can eventually trigger hide/show

Unfortunately, I can't comment on HixField's answer because I don't have enough reputation, so I have to leave this as a separate answer.

His answer is missing the computed property for movedFrameView, which is:

var movedFrameView:CGRect? {
  get { return objc_getAssociatedObject(self, &AssociatedKeys.movedFrameView) as? CGRect }
  set { objc_setAssociatedObject(self, &AssociatedKeys.movedFrameView, newValue, .OBJC_ASSOCIATION_COPY) }
}

My previous answer does not longer work on iOS14. I played with manipulating the frames of the different views, but it seams that the new implementation of the UITabBarController and UITabBar on iOS14 do some magic under the covers which makes this approach no longer working.

I therefore switch to the approach that I hide the UITabBar by setting its alpha to zero and then I manipulate the bottom constraint (that you must pass in when calling the function) to bring the view's content down. This does however, mean that you must have such a constraint and the extension is more bound to your view then the previous approach.

Make sure that the view you are displaying has clipToBounds = false otherwise you will just get a black area where the UITabBar once was!

Here is the code of my UITabBarController.extensions.swift:

import Foundation

extension UITabBarController {
    
    private struct AssociatedKeys {
        // Declare a global var to produce a unique address as the assoc object handle
        static var orgConstraintConstant: UInt8 = 0
        static var orgTabBarAlpha       : UInt8 = 1
    }
    
    var orgConstraintConstant: CGFloat? {
        get { return objc_getAssociatedObject(self, &AssociatedKeys.orgConstraintConstant) as? CGFloat }
        set { objc_setAssociatedObject(self, &AssociatedKeys.orgConstraintConstant, newValue, .OBJC_ASSOCIATION_COPY) }
    }
    
    var orgTabBarAlpha: CGFloat? {
        get { return objc_getAssociatedObject(self, &AssociatedKeys.orgTabBarAlpha) as? CGFloat }
        set { objc_setAssociatedObject(self, &AssociatedKeys.orgTabBarAlpha, newValue, .OBJC_ASSOCIATION_COPY) }
    }
    
    func setTabBarVisible(visible:Bool, animated:Bool, bottomConstraint: NSLayoutConstraint) {
        // bail if the current state matches the desired state
        if (tabBarIsVisible() == visible) { return }
        //define segment animation duration (note we have two segments so total animation time = times 2x)
        let segmentAnimationDuration = animated ? 0.15 : 0.0
        //we should show it
        if visible {
            //animate moving up
            UIView.animate(withDuration:  segmentAnimationDuration,
                           delay: 0,
                           options: [],
                           animations: {
                            [weak self] in
                            guard let self = self else { return }
                            bottomConstraint.constant = self.orgConstraintConstant ?? 0
                            self.view.layoutIfNeeded()
                           },
                           completion: {
                            (_) in
                            //animate tabbar fade in
                            UIView.animate(withDuration: segmentAnimationDuration) {
                                [weak self] in
                                guard let self = self else { return }
                                self.tabBar.alpha = self.orgTabBarAlpha ?? 0
                                self.view.layoutIfNeeded()
                            }
                           })
            //reset our values
            self.orgConstraintConstant = nil
        }
        //we should hide it
        else {
            //save our previous values
            self.orgConstraintConstant = bottomConstraint.constant
            self.orgTabBarAlpha = tabBar.alpha
            //animate fade bar out
            UIView.animate(withDuration:  segmentAnimationDuration,
                           delay: 0,
                           options: [],
                           animations: {
                            [weak self] in
                            guard let self = self else { return }
                            self.tabBar.alpha = 0.0
                            self.view.layoutIfNeeded()
                           },
                           completion: {
                            (_) in
                            //then animate moving down
                            UIView.animate(withDuration: segmentAnimationDuration) {
                                [weak self] in
                                guard let self = self else { return }
                                bottomConstraint.constant = bottomConstraint.constant - self.tabBar.frame.height + 4 // + 4 looks nicer on no-home button devices
                                //self.tabBar.alpha = 0.0
                                self.view.layoutIfNeeded()
                            }
                           })
        }
    }
    
    func tabBarIsVisible() ->Bool {
        return orgConstraintConstant == nil
    }
}

This is how it looks in my app (you can compare to my 1ste answer, the animation is a bit different but looks great) :

animation how it looks

You can have a bug when animating manually the tab bar on iOS13 and Xcode 11. If the user press the home button after the animation (it'll just ignore the animation and will be there in the right place). I think it's a good idea to invert the animation before that by listening to the applicationWillResignActive event.

This wrks for me: [self.tabBar setHidden:YES];
where self is the view controller, tabBar is the id for the tabBar.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top