Imitate Facebook hide/show expanding/contracting Navigation Bar

95,173

Solution 1

The solution given by @peerless is a great start, but it only kicks off an animation whenever dragging begins, without considering the speed of the scroll. This results in a choppier experience than you get in the Facebook app. To match Facebook's behavior, we need to:

  • hide/show the navbar at a rate that is proportional to the rate of the drag
  • kick off an animation to completely hide the bar if scrolling stops when the bar is partially hidden
  • fade the navbar's items as the bar shrinks.

First, you'll need the following property:

@property (nonatomic) CGFloat previousScrollViewYOffset;

And here are the UIScrollViewDelegate methods:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGRect frame = self.navigationController.navigationBar.frame;
    CGFloat size = frame.size.height - 21;
    CGFloat framePercentageHidden = ((20 - frame.origin.y) / (frame.size.height - 1));
    CGFloat scrollOffset = scrollView.contentOffset.y;
    CGFloat scrollDiff = scrollOffset - self.previousScrollViewYOffset;
    CGFloat scrollHeight = scrollView.frame.size.height;
    CGFloat scrollContentSizeHeight = scrollView.contentSize.height + scrollView.contentInset.bottom;

    if (scrollOffset <= -scrollView.contentInset.top) {
        frame.origin.y = 20;
    } else if ((scrollOffset + scrollHeight) >= scrollContentSizeHeight) {
        frame.origin.y = -size;
    } else {
        frame.origin.y = MIN(20, MAX(-size, frame.origin.y - scrollDiff));
    }

    [self.navigationController.navigationBar setFrame:frame];
    [self updateBarButtonItems:(1 - framePercentageHidden)];
    self.previousScrollViewYOffset = scrollOffset;
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    [self stoppedScrolling];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView 
                  willDecelerate:(BOOL)decelerate
{
    if (!decelerate) {
        [self stoppedScrolling];
    }
}

You'll also need these helper methods:

- (void)stoppedScrolling
{
    CGRect frame = self.navigationController.navigationBar.frame;
    if (frame.origin.y < 20) {
        [self animateNavBarTo:-(frame.size.height - 21)];
    }
}

- (void)updateBarButtonItems:(CGFloat)alpha
{
    [self.navigationItem.leftBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem* item, NSUInteger i, BOOL *stop) {
        item.customView.alpha = alpha;
    }];
    [self.navigationItem.rightBarButtonItems enumerateObjectsUsingBlock:^(UIBarButtonItem* item, NSUInteger i, BOOL *stop) {
        item.customView.alpha = alpha;
    }];
    self.navigationItem.titleView.alpha = alpha;
    self.navigationController.navigationBar.tintColor = [self.navigationController.navigationBar.tintColor colorWithAlphaComponent:alpha];
}

- (void)animateNavBarTo:(CGFloat)y
{
    [UIView animateWithDuration:0.2 animations:^{
        CGRect frame = self.navigationController.navigationBar.frame;
        CGFloat alpha = (frame.origin.y >= y ? 0 : 1);
        frame.origin.y = y;
        [self.navigationController.navigationBar setFrame:frame];
        [self updateBarButtonItems:alpha];
    }];
}

For a slightly different behavior, replace the line that re-positions the bar when scrolling (the else block in scrollViewDidScroll) with this one:

frame.origin.y = MIN(20, 
                     MAX(-size, frame.origin.y - 
                               (frame.size.height * (scrollDiff / scrollHeight))));

This positions the bar based on the last scroll percentage, instead of an absolute amount, which results in a slower fade. The original behavior is more Facebook-like, but I like this one, too.

Note: This solution is iOS 7+ only. Be sure to add the necessary checks if you're supporting older versions of iOS.

Solution 2

EDIT: Only for iOS 8 and above.

You can try use

self.navigationController.hidesBarsOnSwipe = YES;

Works for me.

If your coding in swift you have to use this way (from https://stackoverflow.com/a/27662702/2283308)

navigationController?.hidesBarsOnSwipe = true

Solution 3

Here is one more implementation: TLYShyNavBar v1.0.0 released!

I decided to make my own after trying the solutions provided, and to me, they were either performing poorly, had a a high barrier of entry and boiler plate code, or lacked the extension view beneath the navbar. To use this component, all you have to do is:

self.shyNavBarManager.scrollView = self.scrollView;

Oh, and it is battle tested in our own app.

Solution 4

You can have a look at my GTScrollNavigationBar. I have subclassed UINavigationBar to make it scroll based on the scrolling of a UIScrollView.

Note: If you have an OPAQUE navigation bar, the scrollview must EXPAND as the navigation bar gets HIDDEN. This is exactly what GTScrollNavigationBar does. (Just as in for example Safari on iOS.)

Solution 5

iOS8 includes properties to get the navigation bar hiding for free. There is a WWDC video that demonstrates it, search for "View Controller Advancements in iOS 8".

Example:

class QuotesTableViewController: UITableViewController {

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)

    navigationController?.hidesBarsOnSwipe = true
}

}

Other properties:

class UINavigationController : UIViewController {

    //... truncated

    /// When the keyboard appears, the navigation controller's navigationBar toolbar will be hidden. The bars will remain hidden when the keyboard dismisses, but a tap in the content area will show them.
    @availability(iOS, introduced=8.0)
    var hidesBarsWhenKeyboardAppears: Bool
    /// When the user swipes, the navigation controller's navigationBar & toolbar will be hidden (on a swipe up) or shown (on a swipe down). The toolbar only participates if it has items.
    @availability(iOS, introduced=8.0)
    var hidesBarsOnSwipe: Bool
    /// The gesture recognizer that triggers if the bars will hide or show due to a swipe. Do not change the delegate or attempt to replace this gesture by overriding this method.
    @availability(iOS, introduced=8.0)
    var barHideOnSwipeGestureRecognizer: UIPanGestureRecognizer { get }
    /// When the UINavigationController's vertical size class is compact, hide the UINavigationBar and UIToolbar. Unhandled taps in the regions that would normally be occupied by these bars will reveal the bars.
    @availability(iOS, introduced=8.0)
    var hidesBarsWhenVerticallyCompact: Bool
    /// When the user taps, the navigation controller's navigationBar & toolbar will be hidden or shown, depending on the hidden state of the navigationBar. The toolbar will only be shown if it has items to display.
    @availability(iOS, introduced=8.0)
    var hidesBarsOnTap: Bool
    /// The gesture recognizer used to recognize if the bars will hide or show due to a tap in content. Do not change the delegate or attempt to replace this gesture by overriding this method.
    @availability(iOS, introduced=8.0)
    unowned(unsafe) var barHideOnTapGestureRecognizer: UITapGestureRecognizer { get }
}

Found via http://natashatherobot.com/navigation-bar-interactions-ios8/

Share:
95,173
El Mocoso
Author by

El Mocoso

Updated on August 09, 2020

Comments

  • El Mocoso
    El Mocoso over 3 years

    In the new iOS7 Facebook iPhone app, when the user scrolls up the navigationBar gradually hides itself to a point where it completely vanishes. Then when the user scrolls down the navigationBar gradually shows itself.

    How would you implement this behavior yourself? I am aware of the following solution but it disappears right away and it isn't tied to the speed of the user's scroll gesture at all.

    [navigationController setNavigationBarHidden: YES animated:YES];
    

    I hope this isn't a duplicate as I'm not sure how best to describe the "expanding/contracting" behavior.

  • Diana Sule
    Diana Sule over 10 years
    Found similar post here
  • El Mocoso
    El Mocoso over 10 years
    Thanks Diana! I suspected I might have to implement the UIScrollViewDelegate methods but it seemed like it might be a little overkill. Once I figure this thing out I'll post it up. Cheers!
  • Dhanush
    Dhanush over 10 years
    This works. Except you are assuming the bar button items have custom views. In my case I don't have a custom view. So the above does not hide the bar buttons. I think @peerless solution is better for hiding and showing navbar items
  • Wayne
    Wayne over 10 years
    You are right. I may look into a more general solution this weekend. Shouldn't be too difficult to target the default items instead of customView.
  • Wayne
    Wayne over 10 years
    I haven't addressed @Dhanush's issue but I do have an update.
  • Xu Yin
    Xu Yin over 10 years
    Thank you @Iwburk for your answer. Do you have any update for the fade part? Thanks
  • hacker
    hacker over 10 years
    @lwburk when i pushing another view controller from here it has the same behaviour which i dot want.but i need my table view to hold its position ?can u guide me?
  • Legoless
    Legoless over 10 years
    I fixed the issue with stock bar buttons, but this code has another issue. If ScrollView's contentSize is smaller than frame, the animation of sliding does not work. Also make sure you reset all navigation item's alpha back to 1.0 in viewDidDisappear.
  • Wayne
    Wayne about 10 years
    That's true. Furthermore, you should add some flag to prevent these animations from happening any time the scrollView isn't showing.
  • Wayne
    Wayne about 10 years
    @Legoless - What behavior do you see when the contentSize is smaller than the frame? Are you simply returning if that's the case? Also, please share your stock button fix.
  • CedricSoubrie
    CedricSoubrie about 10 years
    Great job. Just a question : I didn't manage to change your code to make the scrollview height change now that there is more space to display it (I have an opaque navbar). Can you help on that ?
  • Wayne
    Wayne about 10 years
    @CedricSoubrie - I don't completely understand your question
  • Aliaksandr B.
    Aliaksandr B. about 10 years
    Did you check this solution on controllers which are used AutoLayout? In my case everything is working fine without AutoLayout, but when it turn on, it still be visible some strange white strip under it
  • Fattie
    Fattie about 10 years
    BTW for anyone reading, exactly how to call initWithNavigationBarClass ... stackoverflow.com/questions/22286166
  • Fattie
    Fattie about 10 years
    @CedricSoubrie bjr, just as you say If you have an opaque navigation bar, the scrollview must expand while the navigation bar gets hidden ... I believe Iwb's solution presented does NOT take care of that situation. in fact, in that situation, just use GTScrollNavigationBar in the answer below, which exactly does that. Cheers.
  • thisiscrazy4
    thisiscrazy4 about 10 years
    Is there any other way than looping through the subviews?
  • blueice
    blueice about 10 years
    From what I could tell on a non custom navigation bar there are 4 total subviews: _UINavigationBarBackground, UINavigationItemView, UINavigationItemButtonView, and _UINavigationBarBackIndicatorView. The loop is pretty quick and does not seem to affect any performance of my app.
  • blueice
    blueice about 10 years
    Since _UINavigationBarBackground seems to always be the first subview you could just access the rest directly: ((UIView *)self.navigationController.navigationBar.subviews[1]).alpha = alpha;
  • thisiscrazy4
    thisiscrazy4 about 10 years
    How can we also imitate the contracting toolbar underneath the nav bar?
  • Mazyod
    Mazyod almost 10 years
    @TimArnold Thanks for your feedback! I have fixed that issue, just didn't update the pod yet >_< will do that right now! .. Pod updated!
  • Tim Arnold
    Tim Arnold almost 10 years
    I'll give it another shot! Thanks!
  • Tim Arnold
    Tim Arnold almost 10 years
    I appreciate your help. Looks like your commit helped. I'm still getting a strange issue where my UICollectionView isn't properly resized as the nav bar is, and so cells going up where the nav bar USED to be are clipped, as they are outside the collection view bounds. Do you know why this might be happening?
  • Tim Arnold
    Tim Arnold almost 10 years
    Figures: found my solution seconds after writing this comment. I had to make sure extendedLayoutIncludesOpaqueBars was set to YES on my UICollectionViewController
  • Mazyod
    Mazyod almost 10 years
    @TimArnold That's awesome! There was another guy having the same problem, and hopefully your solution will help him.
  • user1068810
    user1068810 almost 10 years
    @Iwburk Great Solution, But my requirement is a bit different though my navigation bar will be hidden initially and will be shown on table view scrolling. Please can you refer the Google+ app for the scenario, anybody's public profile.
  • Tim Arnold
    Tim Arnold over 9 years
    @Mazyod ran into another issue: when scrolled down a list, with shy nav bar hidden, when the view controller is about to disappear (e.g. pushing another view controller because user tapped on a cell), the navigation bar abruptly (without animation) appears briefly before the view controller transition. Any idea how to fix this?
  • Mazyod
    Mazyod over 9 years
    Hey @TimArnold. I appreciate you reporting these issues, it's just that it's better to keep them in one place by reporting them on github :) After all, it's open source, and anyone might implement the fix.
  • Tim Arnold
    Tim Arnold over 9 years
    Whoops, yep, that's a great idea, my apologies. I'll post an Issue on Github
  • Katedral Pillon
    Katedral Pillon over 9 years
    If the navigation bar has children views (i.e. Items such as left/right buttons, UISearchBar), then the shrinkage leaves a black background where the navigation bar used to be.
  • aherrick
    aherrick over 9 years
    awesome solution! It's working for me however I'm trying to play with the best way to show/hide the navigation bar text. when the nav bar shrinks up you can see half the text of the title. where would be the best place to clear/re-add the nav bar title? self.navigationItem.title = @"" or self.navigationItem.title = @"My App"
  • kAiN
    kAiN over 9 years
    Hello guys, this works well for me but if I have a tableview with many cell but if my tableView cells has only 4 when I scroll the nav bar is hidden immediately without animation and it looks really bad .. how can I fix ... I can say the exact point you want to edit?
  • Wayne
    Wayne over 9 years
    @rory You know, you're right. I remember having this same issue, but I don't recall exactly how I fixed it. Sorry. I'm just leaving this comment to confirm the issue for future users of this code.
  • kAiN
    kAiN over 9 years
    @Iwburk We can not find a solution in any way? to understand how to tackle the problem?
  • aherrick
    aherrick over 9 years
    @Thuy great work man! So I have this working on perfectly on a table view controller except one thing... I have pull to refresh implemented at the top. It gets weird when trying to pull down and refresh. Might there be a workaround for this?
  • aherrick
    aherrick over 9 years
    @Thuy also... lets say I had a view controller where I've implemented a table view at the bottom. I want to hook up to that table view which works, however I have another view that is sitting above the table view that I want to disappear as well. How might this work?
  • g_pass
    g_pass over 9 years
    Any thoughts on adapting this solution so it plays well with the in-call / personal hotspot status bar notifications?? Otherwise it's pretty sweet
  • g_pass
    g_pass over 9 years
    It would also be great if there was a way to make the status bar transparent after the nav bar disappears - right now it stays the color of my nav bar
  • Andrew
    Andrew over 9 years
    I used frame.origin.y = MIN(20, MAX(-size, frame.origin.y - 4 *(frame.size.height * (scrollDiff / scrollHeight)))) (added the multiplier of 4) for a nice transition speed.
  • Rudolf J
    Rudolf J over 9 years
    Thanks for the code! You have to however change it in such a way that the viewcontroller that is displayed, also changes it size with it.
  • trdavidson
    trdavidson over 9 years
    great piece of code - only problem is that the views that are now in the position where the navbar used to be are not 'selectable' - anybody have any idea how to circumvent this? thanks!
  • Gökhan Çokkeçeci
    Gökhan Çokkeçeci over 9 years
    stackoverflow.com/questions/28127482/… please check this question i used your code
  • Petar
    Petar about 9 years
    this is not available in iOS < 8.0
  • Pedro Romão
    Pedro Romão about 9 years
    As pet60t0 said, and I am apologize, only works for iOS 8 and above.
  • C0D3
    C0D3 about 9 years
    How do you bring it back after that?
  • Pedro Romão
    Pedro Romão about 9 years
    the behaviour it's a bit strange. I didn't explore much, but if you scroll faster it shows again.
  • Ruben Marin
    Ruben Marin almost 9 years
    It works perfectly and I have managed to adapt it to a UIPageViewController content controller only adding the corresponding frame resizing on your methods the same way you do with the nav item.
  • Chisx
    Chisx almost 9 years
    Im trying to implement this and test this right now, what is previousScrollViewYOffset and where and how should it be set is my only question?
  • Wayne
    Wayne almost 9 years
    @Chisx - It's a property that you'll need to create. I updated the post with that information.
  • Brian Nezhad
    Brian Nezhad over 8 years
    @Mazyod do you know how can I Apply this to bottom toolbar?
  • Mazyod
    Mazyod over 8 years
    @NSGod I am assuming you can use the concept found in this component, but it doesn't support the bottom toolbar out of the box. Try to hack it, it's pretty simple (I think)
  • ryder
    ryder over 8 years
    @WayneBurkett what changes would need to be made for a standard title in the nav bar not using a custom view?
  • abhimuralidharan
    abhimuralidharan about 8 years
    How can i add a button to this customHeaderView which will hide when I scroll up.I don't need static button.Is it possible?I tried creating one button as a subview.But it is not receiving any touches.
  • user3354805
    user3354805 about 8 years
    @WayneBurkett I had the same issues as rory had, I used a custom view instead of navigation bar The issue was if a table view or collection view doesn't have enough cells this particular code was called if (scrollOffset <= -scrollView.contentInset.top) { frame.origin.y = 20; } I used a temporary solution in which we identify the direction of scroll and set the 'frame.origin.y = 20' only when the scroll was dragging down
  • skensell
    skensell about 8 years
    I did something similar to this, and this is easily the best solution. I've tried several hacks and libraries, and this is the only one which works for iOS 9 with a tableView not covering the whole screen. Kudos!
  • Hardik Thakkar
    Hardik Thakkar about 8 years
    Thanks a lot.. prefect demo
  • Imran
    Imran almost 8 years
    How do I make it shrink only half?
  • Joris Mans
    Joris Mans almost 8 years
    FYI this does not always work correctly. I tried this too and it works as long as you are not "bouncing" the scrollview. It seems KVO is not triggered when in the bounce part. The delegate call for the contentOffset is triggered though.
  • Gagan_iOS
    Gagan_iOS over 7 years
    @PedroRomão great answer.
  • nambatee
    nambatee over 6 years
    @WayneBurkett do you know how to get rid of the strip that appears underneath the nav bar when you swipe it away? Here's what it looks like: link
  • Frostmourne
    Frostmourne about 4 years
    this shows jerky behaviour on WKWebView.scrollview, any suggestions?
  • Ely
    Ely about 4 years
    navigationBar.height? scrollView.height? Do you use an extension on the frame property?
  • David_2877
    David_2877 about 3 years
    Can you add on the code for hiding the Bottom TabBar with the same effect please?