129

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.

Wayne
  • 56,476
  • 13
  • 125
  • 118
El Mocoso
  • 1,400
  • 3
  • 9
  • 7
  • 2
    Same issues: http://stackoverflow.com/questions/21929220/show-hide-uitoolbar-match-finger-movement-precisely-as-in-for-example-ios7-s Note that it is **incredibly difficult** to **absolutely** match the Safari behaviour. There are some very, very complicated rules in there! – Fattie Feb 28 '14 at 20:41
  • 1
    In my project I used [this project](https://github.com/ninjinkun/NJKScrollFullScreen) and it worked just fine. Take a look at its documentation. – Vinicius Mar 12 '14 at 19:29
  • https://github.com/bryankeller/BLKFlexibleHeightBar/ will let you do what you want and more. It lets you specify exactly how the bar looks at each stage of its transition from maximized to minimized. It even lets you specify your own behaviors, so it can act like Safari, Facebook, or some other app. – blkhp19 Mar 12 '15 at 22:46
  • I did not use a uinavigationbar but added a uiview instead. The view replicating the navigation bar will expand and contract based on scroll. I used scrollViewDidScroll delegate method to achieve the task. You might want to check and execute the source code below.. https://www.dropbox.com/s/b2c0zw6yvchaia5/FailedBanks.zip?dl=0 – Deepak Thakur Jan 03 '16 at 17:37

20 Answers20

162

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.

Wayne
  • 56,476
  • 13
  • 125
  • 118
  • 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 – Dhanush Jan 10 '14 at 21:25
  • 1
    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 Jan 10 '14 at 21:28
  • I haven't addressed @Dhanush's issue but I do have an update. – Wayne Jan 14 '14 at 20:20
  • Thank you @Iwburk for your answer. Do you have any update for the fade part? Thanks – Xu Yin Jan 16 '14 at 20:11
  • @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? – hacker Jan 17 '14 at 14:25
  • 1
    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`. – Legoless Jan 20 '14 at 12:35
  • That's true. Furthermore, you should add some flag to prevent these animations from happening any time the `scrollView` isn't showing. – Wayne Feb 05 '14 at 05:54
  • @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. – Wayne Feb 05 '14 at 06:34
  • 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 ? – CedricSoubrie Feb 06 '14 at 11:09
  • @CedricSoubrie - I don't completely understand your question – Wayne Feb 10 '14 at 20:39
  • 1
    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 – Aliaksandr B. Feb 23 '14 at 14:47
  • @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. – Fattie Mar 10 '14 at 07:57
  • How can we also imitate the contracting toolbar underneath the nav bar? – thisiscrazy4 Mar 27 '14 at 21:00
  • @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. – user1068810 Jul 11 '14 at 12:45
  • 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" – aherrick Oct 09 '14 at 01:05
  • 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? – kAiN Oct 11 '14 at 15:35
  • @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. – Wayne Oct 12 '14 at 00:18
  • @Iwburk We can not find a solution in any way? to understand how to tackle the problem? – kAiN Oct 12 '14 at 11:31
  • 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 Nov 07 '14 at 02:59
  • 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 – g_pass Nov 07 '14 at 03:00
  • 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. – Andrew Nov 24 '14 at 22:07
  • 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. – Rudolf J Jan 10 '15 at 13:23
  • 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! – trdavidson Jan 15 '15 at 22:36
  • http://stackoverflow.com/questions/28127482/when-i-scroll-nav-bar-gray-area-occur please check this question i used your code – Gökhan Çokkeçeci Jan 24 '15 at 16:23
  • 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. – Ruben Marin May 08 '15 at 13:33
  • 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? – Chisx Jul 02 '15 at 20:54
  • @Chisx - It's a property that you'll need to create. I updated the post with that information. – Wayne Jul 06 '15 at 15:09
  • @WayneBurkett what changes would need to be made for a standard title in the nav bar not using a custom view? – ryder Jan 25 '16 at 15:52
  • @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 – user3354805 Mar 31 '16 at 10:30
  • How do I make it shrink only half? – Imran Jul 10 '16 at 15:40
  • @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](https://imgur.com/b1KK8yp) – nambatee Oct 12 '17 at 12:53
52

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
Community
  • 1
  • 1
Pedro Romão
  • 1,925
  • 23
  • 20
43

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.

Mazyod
  • 21,361
  • 9
  • 86
  • 147
  • @TimArnold Thanks for your feedback! I have fixed that issue, just didn't update the pod yet >_< will do that right now! .. Pod updated! – Mazyod Jul 06 '14 at 20:32
  • I'll give it another shot! Thanks! – Tim Arnold Jul 07 '14 at 02:32
  • 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 Jul 09 '14 at 02:39
  • 4
    Figures: found my solution seconds after writing this comment. I had to make sure `extendedLayoutIncludesOpaqueBars` was set to `YES` on my `UICollectionViewController` – Tim Arnold Jul 09 '14 at 02:42
  • @TimArnold That's awesome! There was another guy having [the same problem](https://github.com/telly/TLYShyNavBar/issues/9), and hopefully your solution will help him. – Mazyod Jul 09 '14 at 06:19
  • @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? – Tim Arnold Aug 27 '14 at 03:07
  • 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. – Mazyod Aug 27 '14 at 15:30
  • Whoops, yep, that's a great idea, my apologies. I'll post an Issue on Github – Tim Arnold Aug 27 '14 at 15:31
  • 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. – Katedral Pillon Sep 14 '14 at 22:18
  • @Mazyod do you know how can I Apply this to bottom toolbar? – Brian Nezhad Aug 03 '15 at 20:38
  • @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) – Mazyod Aug 04 '15 at 17:24
  • Thanks a lot.. prefect demo – Hardik Thakkar Apr 29 '16 at 09:25
  • this shows jerky behaviour on WKWebView.scrollview, any suggestions? – Frostmourne Feb 12 '20 at 10:24
33

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.)

Fattie
  • 30,632
  • 54
  • 336
  • 607
Thuy
  • 985
  • 7
  • 15
  • BTW for anyone reading, exactly how to call initWithNavigationBarClass ... http://stackoverflow.com/questions/22286166 – Fattie Mar 10 '14 at 07:26
  • @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 Oct 17 '14 at 02:03
  • @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? – aherrick Oct 17 '14 at 02:05
25

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/

Ashish Kakkad
  • 22,149
  • 11
  • 88
  • 130
Michael Peterson
  • 9,046
  • 3
  • 51
  • 48
12

I have some kind of a quick and dirty solution for that. Haven't made any in-depth testing but here's the idea:

That property will keep all the items in the navbar for my UITableViewController class

@property (strong, nonatomic) NSArray *navBarItems;

In the same UITableViewController class I have:

-(void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
    if([[[UIDevice currentDevice] systemVersion] floatValue] < 7.0f){
        return;
    }

    CGRect frame = self.navigationController.navigationBar.frame;
    frame.origin.y = 20;

    if(self.navBarItems.count > 0){
        [self.navigationController.navigationBar setItems:self.navBarItems];
    }

    [self.navigationController.navigationBar setFrame:frame];
}

-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if([[[UIDevice currentDevice] systemVersion] floatValue] < 7.0f){
        return;
    }

    CGRect frame = self.navigationController.navigationBar.frame;
    CGFloat size = frame.size.height - 21;

    if([scrollView.panGestureRecognizer translationInView:self.view].y < 0)
    {
        frame.origin.y = -size;

        if(self.navigationController.navigationBar.items.count > 0){
            self.navBarItems = [self.navigationController.navigationBar.items copy];
            [self.navigationController.navigationBar setItems:nil];
        }
    }
    else if([scrollView.panGestureRecognizer translationInView:self.view].y > 0)
    {
        frame.origin.y = 20;

        if(self.navBarItems.count > 0){
            [self.navigationController.navigationBar setItems:self.navBarItems];
        }
    }

    [UIView beginAnimations:@"toggleNavBar" context:nil];
    [UIView setAnimationDuration:0.2];
    [self.navigationController.navigationBar setFrame:frame];
    [UIView commitAnimations];
}

That's only for ios >= 7, it's ugly I know but a quick way to achieve this. Any comments/suggestions are welcome :)

peerless
  • 558
  • 3
  • 11
12

This works for iOS 8 and above and ensures that the status bar still retains its background

self.navigationController.hidesBarsOnSwipe = YES;
CGRect statuBarFrame = [UIApplication sharedApplication].statusBarFrame;
UIView *statusbarBg = [[UIView alloc] initWithFrame:statuBarFrame];
statusbarBg.backgroundColor = [UIColor blackColor];
[self.navigationController.view addSubview:statusbarBg];

And if you want to show the nav bar when you tap on the status bar you can do this:

- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
     self.navigationController.navigationBarHidden = NO;
}
Zhong Huiwen
  • 529
  • 7
  • 8
10

Here is my implementation: SherginScrollableNavigationBar.

In my approach I am using KVO for observing UIScrollView's state, so there is no necessity to use a delegate (and you can use this delegate for whatever else you need).

Valentin Shergin
  • 6,630
  • 2
  • 45
  • 50
  • 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. – Joris Mans Jul 24 '16 at 19:51
7

Please try this solution of mine and let me know why this ain't as good as the previous answers.

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    if (fabs(velocity.y) > 1)
        [self hideTopBar:(velocity.y > 0)];
}

- (void)hideTopBar:(BOOL)hide
{
    [self.navigationController setNavigationBarHidden:hide animated:YES];
    [[UIApplication sharedApplication] setStatusBarHidden:hide withAnimation:UIStatusBarAnimationSlide];
}
Nishant
  • 11,895
  • 9
  • 53
  • 89
  • 1
    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! – skensell Apr 11 '16 at 16:00
6

One way that I’ve accomplished this is the following.

Register your view controller to be the UIScrollViewDelegate of your UITableView for example.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

From within de UIScrollViewDelegate methods you can get the new contentOffset and translate your UINavigationBar up or down accordingly.

Setting the alpha of the subviews can also be done based on some threshold values and factors you can set and compute.

Hope it helps!

Diana Sule
  • 61
  • 3
  • Found similar post [here](http://stackoverflow.com/questions/19363898/animating-uinavigationbar-in-ios-7-like-safari) – Diana Sule Nov 07 '13 at 03:11
  • 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! – El Mocoso Nov 08 '13 at 17:31
4

In addition to Iwburk's answer I added the following to fix the alpha issue on non custom navigation bars and to reset the navigation bar in the viewWillDisappear method:

- (void)updateBarButtonItems:(CGFloat)alpha
{
    for (UIView *view in self.navigationController.navigationBar.subviews) {
        NSString *className = NSStringFromClass([view class]);

        if ( ![className isEqualToString:@"_UINavigationBarBackground"] ) {
            view.alpha = alpha;
        }
    }
}

- (void)resetNavigationBar {
    CGRect frame = self.navigationController.navigationBar.frame;
    frame.origin.y = 20;
    [self.navigationController.navigationBar setFrame:frame];
    [self updateBarButtonItems:1.0f];
}
blueice
  • 136
  • 6
  • Is there any other way than looping through the subviews? – thisiscrazy4 Mar 26 '14 at 23:48
  • 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 Mar 27 '14 at 18:01
  • Since _UINavigationBarBackground seems to always be the first subview you could just access the rest directly: ((UIView *)self.navigationController.navigationBar.subviews[1]).alpha = alpha; – blueice Mar 27 '14 at 18:12
4

I was looking for a solution that allowed for any style and any behavior. You'll notice that bar condensing behavior is different in many different apps. And of course, the way the bar looks is totally different between apps.

I created a solution for this issue with https://github.com/bryankeller/BLKFlexibleHeightBar/

You can definine your own behavior rules to control how and when the bar shrinks and grows, and you can define exactly how you want the bar's subviews to react to the bar condensing or growing.

Have a look at my project if you want a lot of flexibility to make whatever kind of header bar you can think up.

blkhp19
  • 462
  • 1
  • 5
  • 12
  • 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. – abhimuralidharan Mar 15 '16 at 10:31
3

I was trying to emulate this behavior in a situation where I needed a customized header sitting about a UITableView. I rolled my own "navigation" bar because this sits below a bunch of other stuff on the page and I wanted the section headers to follow the default "docking" behavior. I think I found a pretty clever and succinct way to adjust a UITableView/UIScrollView together with another object in a style similar to that seen in the Facebook/Instagram/Chrome/etc. apps.

In my .xib file, I have my components loaded into a freeform view: http://imgur.com/0z9yebJ (sorry, don't have the rep to inline images)

Notice that, in the left sidebar, the table is ordered behind the main header view. You can't tell from the screenshot, but it also has the same y position as the main header view. Since it extends out of sight, the contentInset property on the UITableView set to 76 (the height of the main header view).

To make the main header view slide up in unison with the UIScrollView, I use the UIScrollViewDelegate's scrollViewDidScroll methods to perform some calculations and change the UIScrollView's contentInset as well as the main header view's frame.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    UIEdgeInsets insets = scrollView.contentInset;
    //tableViewInsetDelta and tableViewOriginalInsetValue are NSInteger variables that I set to 0 and 76, respectively, in viewDidLoad
    tableViewInsetDelta = tableViewOriginalInsetValue + scrollView.contentOffset.y;
    insets.top = tableViewOriginalInsetValue - tableViewInsetDelta;

    if (scrollView.contentOffset.y > -76 && scrollView.contentOffset.y < 0) {
        [scrollView setContentInset:insets];
        self.pathTitleContainer.frame = CGRectMake(self.pathTitleContainer.frame.origin.x, 44 - tableViewInsetDelta, self.pathTitleContainer.frame.size.width, self.pathTitleContainer.frame.size.height);
    } else if (scrollView.contentOffset.y > 0) {
        insets.top = 0;
        [scrollView setContentInset:insets];
        self.pathTitleContainer.frame = CGRectMake(self.pathTitleContainer.frame.origin.x, -32, self.pathTitleContainer.frame.size.width, self.pathTitleContainer.frame.size.height);
    } else if (scrollView.contentOffset.y < -76) {
        insets.top = 76;
        [scrollView setContentInset:insets];
        self.pathTitleContainer.frame = CGRectMake(self.pathTitleContainer.frame.origin.x, 44, self.pathTitleContainer.frame.size.width, self.pathTitleContainer.frame.size.height);
    }
}

The first if statement does most of the heavy lifting, but I had to include the other two to handle situations where the user is dragging forcefully and the initial contentOffset values sent to scrollViewDidScroll are outside of the range of the first if statement.

Ultimately, this is working really well for me. I hate loading up my projects with a bunch of bloated subclasses. I can't speak to whether this is the best solution performance-wise (I've always been hesitant to put any code in scrollViewDidScroll since it gets called all the time), but the code footprint is the smallest I've seen in any solution for this problem and it doesn't involve nesting a UITableView in a UIScrollView (Apple advises against this in the documentation and touch events end up a bit funky on the UITableView). Hope this helps someone!

Brian
  • 191
  • 1
  • 10
3

HidingNavigationBar a great project that hides the Navigation Bar and the Tab Bar if you want.

HidingNavigationBar supports hiding/showing of the following view elements:

UINavigationBar

UINavigationBar and an extension UIView

UINavigationBar and a UIToolbar

UINavigationBar and a UITabBar

https://github.com/tristanhimmelman/HidingNavigationBar

Michael Peterson
  • 9,046
  • 3
  • 51
  • 48
2

I tried implementing GTScrollNavigationBar but my app required me to modify auto layout constraints. I decided to put an example of my implementation up on GitHub in case anyone else has to do this with auto layout. The other issue I had with most of the other implementations is that people don't set the bounds of the scroll view to avoid the parallax scrolling effect that you create while you scroll and adjust the size of the scrollview simultaneously.

Check out JSCollapsingNavBarViewController if you need to do this with auto layout. I've included two versions, one with the nav bar only and another with a sub-bar below the nav bar which collapses before collapsing the nav bar.

jwswart
  • 1,096
  • 12
  • 15
1

i tried it with this way, i hope it will help. just implement the code in delegate method and set to to the desired view/subview

-(void)scrollViewDidScroll:(UIScrollView *)scrollView{ 
            CGRect frame=self.view.frame;
            CGRect resultFrame=CGRectZero;
            if(scrollView.contentOffset.y==0 || scrollView.contentOffset.y<0){
                self.lastContentOffset=0;
                self.offset=0;
                resultFrame=CGRectMake(0, frame.size.height-(40-self.offset.intValue), frame.size.width, 40-self.offset.intValue);
    // Pass the resultFrame
                [self showHide:YES withFrame:resultFrame];
            }else if (self.lastContentOffset > scrollView.contentOffset.y){
                NSNumber *temp=[NSNumber numberWithDouble:self.lastContentOffset-scrollView.contentOffset.y];
                if(temp.intValue>40 || self.offset.intValue<temp.intValue){
                    self.offset=[NSNumber numberWithInt:0];
                    resultFrame=CGRectMake(0, frame.size.height-(40-self.offset.intValue), frame.size.width, 40-self.offset.intValue);
    // Pass the resultFrame
                    [self showHide:YES withFrame:resultFrame];
                }else{
                    if(temp.intValue>0){
                        self.offset=[NSNumber numberWithInt:self.offset.intValue-temp.intValue];
                        resultFrame=CGRectMake(0, frame.size.height-(40-self.offset.intValue), frame.size.width, 40-self.offset.intValue);
    // Pass the resultFrame
                        [self showHide:YES withFrame:resultFrame];
                    }
                }
            }else if (self.lastContentOffset < scrollView.contentOffset.y){
                NSNumber *temp=[NSNumber numberWithDouble:scrollView.contentOffset.y-self.lastContentOffset];
                if(self.offset.intValue>40 || (self.offset.intValue+temp.intValue)>40){
                    self.offset=[NSNumber numberWithInt:40];
    // Pass the resultFrame
                    [self showHide:NO withFrame:resultFrame];
                }else{
                    self.offset=[NSNumber numberWithInt:self.offset.intValue+temp.intValue];
                    resultFrame=CGRectMake(0, frame.size.height-(40-self.offset.intValue), frame.size.width, 40-self.offset.intValue);
    // Pass the resultFrame
                    [self showHide:YES withFrame:resultFrame];
                }
            }
            self.lastContentOffset = scrollView.contentOffset.y;

        }

-(void)showHide:(Boolean)boolView withFrame:(CGRect)frame{
               if(showSRPFilter){
                        //Assign value of "frame"to any view on which you wan to to perform animation
                }else{
                       //Assign value of "frame"to any view on which you wan to to perform animation
                }
        }
user2968901
  • 87
  • 2
  • 7
1

An extension of @Iwburk 's answer... Instead of changing the origin of the navigation bar, I needed to expand/shrink the size of the navigation bar.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    CGRect frame = self.previousRect; // a property set in the init method to hold the initial size of the uinavigationbar
    CGFloat size = frame.size.height;
    CGFloat framePercentageHidden = ((MINIMUMNAVBARHEIGHT - 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 = -MINIMUMNAVBARHEIGHT;
    } else if ((scrollOffset + scrollHeight) >= scrollContentSizeHeight) {
        frame.origin.y = -size;
    } else {
        frame.origin.y = MIN(-MINIMUMNAVBARHEIGHT, MAX(-size, frame.origin.y - scrollDiff));
    }

    self.previousRect = CGRectMake(0, frame.origin.y, self.jsExtendedBarView.frame.size.width, 155);
    self.layoutConstraintExtendedViewHeight.constant = MAXIMUMNAVBARHEIGHT + frame.origin.y + MINIMUMNAVBARHEIGHT;
    [self updateBarButtonItems:(1 - framePercentageHidden)];
    self.previousScrollViewYOffset = scrollOffset;
}

It doesn't work with the stoppedScrolling method yet, ill post an update when I have it

jsetting32
  • 1,566
  • 2
  • 16
  • 43
1

for Swift 4,5 - iOS 11 and above

private var previousScrollViewYOffset: CGFloat = 0
private var firstLoad = true
// to avoid scrollViewDidScroll called when first time view controller load
override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        firstLoad = false
    }
// MARK: - UIScrollViewDelegate
extension ViewController: UIScrollViewDelegate {
    func stoppedScrolling() {
        let frame = self.navigationController?.navigationBar.frame ?? .zero
        if frame.origin.y < UIView.statusBarFrame.size.height {
            self.animateNavBar(to: -frame.size.height + UIView.statusBarFrame.size.height)
        }
    }
    func updateBarButtonItems(alpha: CGFloat) {
        self.navigationItem.leftBarButtonItems?.forEach{ item in
            item.customView?.alpha = alpha
        }
        self.navigationItem.rightBarButtonItems?.forEach{ item in
            item.customView?.alpha = alpha
        }
        self.navigationItem.titleView?.alpha = alpha
        self.navigationController?.navigationBar.tintColor = self.navigationController?.navigationBar.tintColor.withAlphaComponent(alpha)
    }
    
    func animateNavBar(to y: CGFloat) {
        UIView.animate(withDuration: 0.2) {[weak self] in
            var frame: CGRect = self?.navigationController?.navigationBar.frame ?? .zero
            let alpha: CGFloat = frame.origin.y >= y ? 0 : 1
            frame.origin.y = y
            self?.navigationController?.navigationBar.frame = frame
            self?.updateBarButtonItems(alpha: alpha)
        }
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if firstLoad { return }
        var frame = self.navigationController?.navigationBar.frame ?? .zero
        let size = frame.size.height - UIView.statusBarFrame.size.height
        let framePercentageHidden = (UIView.statusBarFrame.size.height - frame.origin.y) / (frame.size.height - 1)
        let scrollOffset = scrollView.contentOffset.y
        let scrollDiff = scrollOffset - previousScrollViewYOffset
        let scrollHeight = scrollView.frame.size.height
        let scrollContentSizeHeight = scrollView.contentSize.height + scrollView.contentInset.bottom
        if scrollOffset <= -scrollView.contentInset.top {
            frame.origin.y = UIView.statusBarFrame.size.height
        } else if ((scrollOffset + scrollHeight) >= scrollContentSizeHeight) {
            frame.origin.y = -size
        } else {
            frame.origin.y = min(UIView.statusBarFrame.size.height, max(-size, frame.origin.y - scrollDiff))
        }
        self.navigationController?.navigationBar.frame = frame
        self.updateBarButtonItems(alpha: 1 - framePercentageHidden)
        self.previousScrollViewYOffset = scrollOffset
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        self.stoppedScrolling()
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if(!decelerate) {
            self.stoppedScrolling()
        }
    }
}

UIView extension

extension UIView {
    public static var statusBarFrame: CGRect {
        get {
            return UIApplication.shared.statusBarFrame
        }
    }
}

You should custom navigationItem.titleView to apply set alpha

Giang
  • 1,576
  • 2
  • 19
  • 22
0

All of these approaches seem overly complicated... So naturally, I built my own:

class ViewController: UIViewController, UIScrollViewDelegate {
    var originalNavbarHeight:CGFloat = 0.0
    var minimumNavbarHeight:CGFloat = 0
    weak var scrollView:UIScrollView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        // setup delegates 
        scrollView.delegate = self
        // save the original nav bar height
        originalNavbarHeight = navigationController!.navigationBar.height
    }


    func scrollViewDidScroll(scrollView: UIScrollView) {
        // will relayout subviews
        view.setNeedsLayout() // calls viewDidLayoutSubviews
    }

    override func viewDidLayoutSubviews() {
        var percentageScrolled = min(scrollView.contentOffset.y / originalNavbarHeight, 1)
        navigationController?.navigationBar.height = min(max((1 - percentageScrolled) * originalNavbarHeight, minimumNavbarHeight), originalNavbarHeight)
        // re-position and scale scrollview
        scrollView.y = navigationController!.navigationBar.height + UIApplication.sharedApplication().statusBarFrame.height
        scrollView.height = view.height - scrollView.y
    }

    override func viewWillDisappear(animated: Bool) {
        navigationController?.navigationBar.height = originalNavbarHeight
    }

}
Oxcug
  • 6,034
  • 2
  • 28
  • 42
  • navigationBar.height? scrollView.height? Do you use an extension on the `frame` property? – Ely Mar 08 '20 at 08:55
0

I found all answers given in Objective-C. This is my answer in Swift 3. This is very generic code and can be used directly. It works with both UIScrollView and UITableView.

var lastContentOffset: CGPoint? = nil
var maxMinus: CGFloat           = -24.0
var maxPlus: CGFloat            = 20.0
var initial: CGFloat            = 0.0

override func viewDidLoad() {
    super.viewDidLoad()

    self.title = "Alarm Details"
    self.lastContentOffset = self.alarmDetailsTableView.contentOffset
    initial = maxPlus
}

func scrollViewDidScroll(_ scrollView: UIScrollView)
{
    var navigationBarFrame: CGRect   = self.navigationController!.navigationBar.frame
    let currentOffset = scrollView.contentOffset

    if (currentOffset.y > (self.lastContentOffset?.y)!) {
        if currentOffset.y > 0 {
            initial = initial - fabs(CGFloat(currentOffset.y - self.lastContentOffset!.y))
        }
        else if scrollView.contentSize.height < scrollView.frame.size.height {
            initial = initial + fabs(CGFloat(currentOffset.y - self.lastContentOffset!.y))
        }
    }
    else {
        if currentOffset.y < scrollView.contentSize.height - scrollView.frame.size.height {
            initial = initial + fabs(CGFloat(currentOffset.y - self.lastContentOffset!.y))
        }
        else if scrollView.contentSize.height < scrollView.frame.size.height && initial < maxPlus {
            initial = initial - fabs(CGFloat(currentOffset.y - self.lastContentOffset!.y))
        }
    }

    initial = (initial <= maxMinus) ? maxMinus : initial
    initial = (initial >= maxPlus) ? maxPlus : initial

    navigationBarFrame.origin.y = initial

    self.navigationController!.navigationBar.frame = navigationBarFrame
    scrollView.frame = CGRect(x: 0.0, y: initial + navigationBarFrame.size.height , width: navigationBarFrame.size.width, height: self.view.frame.size.height - (initial + navigationBarFrame.size.height))

    let framePercentageHidden: CGFloat              = ((20 - navigationBarFrame.origin.y) / (navigationBarFrame.size.height));
    self.lastContentOffset                          = currentOffset;
    self.updateBarButtonItems(alpha: 1 - framePercentageHidden)
}

func updateBarButtonItems(alpha: CGFloat)
{
    self.navigationController?.navigationBar.titleTextAttributes = [NSForegroundColorAttributeName: UIColor.darkGray.withAlphaComponent(alpha)]
    self.navigationController?.navigationBar.isUserInteractionEnabled = (alpha < 1) ? false: true

    guard (self.navigationItem.leftBarButtonItems?.count) != nil else { return }

    for (_, value) in self.navigationItem.leftBarButtonItems!.enumerated() {
        value.customView?.alpha = alpha
    }

    guard (self.navigationItem.rightBarButtonItems?.count) != nil else { return }

    for (_, value) in (self.navigationItem.rightBarButtonItems?.enumerated())! {
        value.customView?.alpha = alpha
    }
}

The logic of setting alpha to navigation items is copied from @WayneBurkett answer and rewritten in Swift 3.

Dev
  • 1,117
  • 8
  • 17