iOS/Swift - Hide/Show UITabBarController when scrolling down/up

21,081

Solution 1

This is code that i'm actually using in a production app.

It's in Swift and it also updates UITabBar.hidden var.

func scrollViewWillBeginDragging(scrollView: UIScrollView) {
    if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0{
        changeTabBar(hidden: true, animated: true)
    }
    else{
        changeTabBar(hidden: false, animated: true)
    }
}

You can also use the other callback method:

func scrollViewDidScroll(scrollView: UIScrollView) {
    ...
}

but if you choose so, then you must handle multiple calls to the helper method that actually hides the tabBar.

And then you need to add this method that animates the hide/show of the tabBar.

func changeTabBar(hidden:Bool, animated: Bool){
    var tabBar = self.tabBarController?.tabBar
    if tabBar!.hidden == hidden{ return }
    let frame = tabBar?.frame
    let offset = (hidden ? (frame?.size.height)! : -(frame?.size.height)!)
    let duration:NSTimeInterval = (animated ? 0.5 : 0.0)
    tabBar?.hidden = false
    if frame != nil
    {
        UIView.animateWithDuration(duration,
            animations: {tabBar!.frame = CGRectOffset(frame!, 0, offset)},
            completion: {
                println($0)
                if $0 {tabBar?.hidden = hidden}
        })
    }
}

Update Swift 4

func changeTabBar(hidden:Bool, animated: Bool){
    guard let tabBar = self.tabBarController?.tabBar else { return; }
    if tabBar.isHidden == hidden{ return }
    let frame = tabBar.frame
    let offset = hidden ? frame.size.height : -frame.size.height
    let duration:TimeInterval = (animated ? 0.5 : 0.0)
    tabBar.isHidden = false

    UIView.animate(withDuration: duration, animations: {
        tabBar.frame = frame.offsetBy(dx: 0, dy: offset)
    }, completion: { (true) in
        tabBar.isHidden = hidden
    })
}

Solution 2

This answer is a slight modification to Ariel answer which adds animation while user scrolls.

extension ViewController:UIScrollViewDelegate{
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0{
            //scrolling down
            changeTabBar(hidden: true, animated: true)
        }
        else{
            //scrolling up
            changeTabBar(hidden: false, animated: true)
        }
    }

    func changeTabBar(hidden:Bool, animated: Bool){
        let tabBar = self.tabBarController?.tabBar
        let offset = (hidden ? UIScreen.main.bounds.size.height : UIScreen.main.bounds.size.height - (tabBar?.frame.size.height)! )
        if offset == tabBar?.frame.origin.y {return}
        print("changing origin y position")
        let duration:TimeInterval = (animated ? 0.5 : 0.0)
        UIView.animate(withDuration: duration,
                       animations: {tabBar!.frame.origin.y = offset},
                       completion:nil)
    }
}

Solution 3

Building on Ariel's answer, I have updated the code for Swift3. This worked great on my collection views.

override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if scrollView.panGestureRecognizer.translation(in: scrollView).y < 0 {
            changeTabBar(hidden: true, animated: true)
        }else{
            changeTabBar(hidden: false, animated: true)
        }

    }

func changeTabBar(hidden:Bool, animated: Bool){
        let tabBar = self.tabBarController?.tabBar
        if tabBar!.isHidden == hidden{ return }
        let frame = tabBar?.frame
        let offset = (hidden ? (frame?.size.height)! : -(frame?.size.height)!)
        let duration:TimeInterval = (animated ? 0.5 : 0.0)
        tabBar?.isHidden = false
        if frame != nil
        {
            UIView.animate(withDuration: duration,
                                       animations: {tabBar!.frame = frame!.offsetBy(dx: 0, dy: offset)},
                                       completion: {
                                        print($0)
                                        if $0 {tabBar?.isHidden = hidden}
            })
        }
    }

Solution 4

You can control UITabBar precisly by setting up your class as delegate for scrollView and implementing scrolling in scrollViewDidScroll: method.

Here is an example how I do it my application. You can probably easily modify that for your needs. Some helper function to get UITabBar included.

#define LIMIT(__VALUE__, __MIN__, __MAX__) MAX(__MIN__, MIN(__MAX__, __VALUE__))

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGFloat scrollOffset = scrollView.contentOffset.y;
    CGFloat scrollDiff = scrollOffset - self.previousScrollViewYOffset;
    CGFloat scrollHeight = scrollView.frame.size.height;
    CGFloat scrollContentSizeHeight = scrollView.contentSize.height + scrollView.contentInset.bottom;
    CGFloat scrollOffsetGlobal = scrollOffset + scrollView.contentInset.top;
    [self updateUITabBarY:[self UITabBarView].frame.origin.y + scrollDiff];
    self.previousScrollViewYOffset = scrollOffset;
}

- (UITabBar*) UITabBarView
{
    for(UIView *view in self.tabBarController.view.subviews)
    {
        if([view isKindOfClass:[UITabBar class]])
        {
            return (UITabBar*) view;
        }
    }

    return nil;
}

- (void) updateUITabBarY:(CGFloat) y
{
    UITabBar* tabBar = [self UITabBarView];
    if(tabBar)
    {
        CGRect frame = tabBar.frame;
        frame.origin.y  = LIMIT(y, [self UITabBarMiny], [self UITabBarMaxY]);
        tabBar.frame = frame;
    }
}

- (CGFloat) UITabBarMiny
{
    return [UIScreen mainScreen].bounds.size.height - [self UITabBarView].frame.size.height - [[UIApplication sharedApplication] statusBarFrame].size.height + 20.0f;
}

- (CGFloat) UITabBarMaxY
{
    return [UIScreen mainScreen].bounds.size.height;
}

Solution 5

Ariels answer works, but has some values that seem off. When you compare the y-value of the scrollView scrollView.panGestureRecognizer.translation(in: scrollView).y, "0" has the side effect, that the tabBar shows or hides when you stop scrolling. It calls the method one more time with a "0" value. I tried it with didEndDragging, didScroll and willBeginDragging with similar effects. And that feels very counter intuitive or buggy. I used +/- 0.1 when comparing the y-value and got the desired effect, that it just shows and hides when you are really scrolling up or down.

Another thing that isn't mentioned is that the offset that you set with tabBar.frame = frame.offsetBy(dx: 0, dy: offset) will be reset when the app moves to the background. You scroll down, the tabBar disappears, you change the app, open it up again, the tabBar is still hidden but the frame is back to the old location. So when the function is called again, the tabBar moves up even more and you have a gap of the size of the tabBar.frame.

To get rid of this I compared the current frame location and animated the alpha value. I couldn't get the usual coming back up animation to work, maybe somebody will try, can't be that hard. But its okay this way, as it doesn't happen that often.

override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {

    let yValue = scrollView.panGestureRecognizer.translation(in: scrollView).y
    if yValue < -0.1 {
        //hide tabBar
        changeTabBar(hidden: true, animated: true)
    } else if yValue > 0.1 {
        //show tabBar
        changeTabBar(hidden: false, animated: true)
    }
}

func changeTabBar(hidden:Bool, animated: Bool) {
    guard let tabBar = self.tabBarController?.tabBar else {
        return
    }
    if tabBar.isHidden == hidden{
        return
    }
    
    let frame = tabBar.frame
    let frameMinY = frame.minY  //lower end of tabBar
    let offset = hidden ? frame.size.height : -frame.size.height
    let viewHeight = self.view.frame.height
    
    //hidden but moved back up after moving app to background
    if frameMinY < viewHeight && tabBar.isHidden {
        tabBar.alpha = 0
        tabBar.isHidden = false

        UIView.animate(withDuration: 0.5) {
            tabBar.alpha = 1
        }
        
        return
    }

    let duration:TimeInterval = (animated ? 0.5 : 0.0)
    tabBar.isHidden = false

    UIView.animate(withDuration: duration, animations: {
        tabBar.frame = frame.offsetBy(dx: 0, dy: offset)
    }, completion: { (true) in
        tabBar.isHidden = hidden
    })
}
Share:
21,081
Philipp B.
Author by

Philipp B.

Updated on July 09, 2022

Comments

  • Philipp B.
    Philipp B. almost 2 years

    I'm quite new to iOS development. Right now i'm trying to hide my tabbar when I scroll down and when scrolling up the tabbar should appear. I would like to have this animated in the same way like the navigation bar. For the navigation bar I simply clicked the option in the Attributes Inspector. I saw some examples for the toolbar, but I cant adopt it the tabbar.

    self.tabBarController?.tabBar.hidden = true just hides my tabbar, but its not animated like the navigation controller.

  • Philipp B.
    Philipp B. almost 9 years
    Thanks for your reply, i'll try to implement it later in my project
  • Philipp B.
    Philipp B. almost 9 years
    Thanks for ur answer. I'll try the swift answer first.
  • Philipp B.
    Philipp B. almost 9 years
    Works great. This is exactly what i was looking for. Thanks.
  • Grzegorz Krukowski
    Grzegorz Krukowski over 8 years
    Sry I added missing macro for you - it just limits first parameter to a certain range - (value, MIN, MAX)
  • houguet pierre
    houguet pierre about 8 years
    Great answer. Thanks
  • baskInEminence
    baskInEminence over 7 years
    I found scrollViewWillEndDragging to be smoother for the user
  • Ariel Hernández Amador
    Ariel Hernández Amador over 7 years
    The question asks for showing the tabBar when re-starting to scrolls, that's why i'm changing it scrollViewWillBeginDragging. But scrollViewWillEndDragging will do the same trick.
  • Eshwaren Manoharen
    Eshwaren Manoharen about 7 years
    The code works with a tableviewcontroller. But when the tab bar hides, there seems to be a black area when the tab was at. Anyway to fix this or I am missing something?
  • Ariel Hernández Amador
    Ariel Hernández Amador about 7 years
    It hides the tabBar and shows the view below the tabBar, it's show you a black area, because the tableViewController is not covering that area.
  • Gurjit Singh
    Gurjit Singh over 5 years
    @ArielHernándezAmador for this remove the bottom from safe area and add it from superview then its work well.
  • Ali Ihsan URAL
    Ali Ihsan URAL over 5 years
    I set bottom constraint to Superview but the black screen doesnt hide. Where is the mistake ? Thanks for reply
  • shubham mishra
    shubham mishra over 4 years
    For me, this solution was not working, but then I found some constraints issue on my end, I have given bottom view's bottom constraint with respect to safe area then, I changed it to superView and it worked for me.(stackoverflow.com/a/55216583/7516788)
  • Dylan
    Dylan over 3 years
    @AliIhsanURAL I have the same problem, it still black even I placed the view constraint to superview instead of safe area. Any update on this?