UINavigationBar that scrolls away

(UPDATE: This solution seems to not play well with iOS7 at the moment. Please contact me if you find a solution for iOS7, thanks!)

At some point when working with scrollable content on the iPhone, one might find that the navigation bar just takes up valuable screen space with UI elements that you don’t always need access to. So for me, this is dead space.

Apps have often implemented a sort of auto-hide for their view controller’s navigation, and whether you like this approach is a matter of taste and design. I’m a software developer first, but I also have an eye for aesthetics and enjoy eye candy, and new approaches to old problems.

What is all this preamble about? I found a way to make the navigation bar scroll smoothly away with your UIScrollView (or UITableView which is a direct subclass), so that the navigation bar is only visible if you’re at the top of your scroll view.

Sadly however, the key to this functionality being awesome is knowing that, as a user, if you touch the status bar on the iPhone a scrollview (or table view) will scroll back to the top, you’re only ever one tap away from your navigation bar. Sadly most iPhone users don’t know this, I don’t think.

As you’ve possibly seen in my post about Auto-snap to table view sections or my posts that discuss the creation of a baseclass UIViewController for your projects (i.e. subclass UIViewController with general functionality that will be present throughout your app), I will add some functionality to the baseclass’ scrollview delegate implementation. The beauty of doing this is that any subclass can (or not) use that functionality just by calling super (I write this not knowing the level of experience of the reader, as some are experienced Master’s degree holders, and some are self-taught hacks who “get it” for the most part but haven’t had the advantage of the theory provided through a proper(ly expensive) education).

How is this achieved? We get a little bit into QuartzCore and basically make you think the UINavigationBar has disappeared, but we make use of the fact that UIView objects basically wrap underlying QuartzCore drawing layers. What? Yeah, we keep the UINavigationBar’s frame where it is, but shift the position of the underlying CALayer it is controlling.

Give me code, dammit!

OK.

#import <UIKit/UIKit.h>

@interface MyBaseViewController : UIViewController
{
    @protected
    __weak UIScrollView *_scroller;  // we keep a weak pointer because, well, we don't need a strong pointer.
    CGFloat _scrollViewContentOffsetYThreshold;  // defines at what contentOffset of the above scrollView the navigationBar should start scrolling

    // other stuff related to your app here...
}

- (void)scrollViewDidScroll:(UIScrollView*)aScrollView;  // in this context, this method is more of a helper.  Your subclasses can call it if they want to.

@end

So what have we set up here? We define a weak pointer as a protected variable, meaning any subclass can access it. Then we need a variable that you set, based on your app’s requirements. It may be that your UITableView’s first row doesn’t start flush with the bottom of the navigationBar. This value _scrollViewContentOffsetYThreshold is just to allow you to fine tune.

Then we add a helper method for any of your subclasses that have a UIScrollView (TableView), whose delegate method implementation (in your subclass) can just call this method to enable this scrolling navigation bar functionality.

Now the .m file:

#import "MyBaseViewController.h"
#import <QuartzCore/QuartzCore.h>

#define kNavBarDefaultPosition CGPointMake(160, 22) // we need this for later.  This is (iPhone) the center coordinate of a navigationBar in portrait mode.

@implementation MyViewController

// ... your other methods here as well
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    // now check for a scrollview (which you've probably set in viewDidLoad)
    if(_scroller)
    {
        // get the navigation bar's underlying CALayer object
        CALayer *layer = self.navigationController.navigationBar.layer;
        CGFloat contentOffsetY = _scroller.contentOffset.y;

   	// if the scrolling is not at the top and has passed the threshold, then set the navigationBar layer's position accordingly.
        if (contentOffsetY > _scrollViewContentOffsetYThreshold) {
            layer.position = CGPointMake(layer.position.x,
                                         22 - MIN((contentOffsetY - _scrollViewContentOffsetYThreshold), 48.0));
        }
        else
        {
            layer.position = kNavBarDefaultPosition;  // otherwise we are at the top and the navigation bar should be seen as if it can't scroll away.
        }

    }
}

// if your subclasses have scrollviews, and you want the navigation bar to only be present when you are at the top, you can call this method in your scrollViewDelegate.

- (void)scrollViewDidScroll:(UIScrollView*)aScrollView
{
    if(_scroller == aScrollView){

	CALayer *layer = self.navigationController.navigationBar.layer;

	CGFloat contentOffsetY = aScrollView.contentOffset.y;

	if (contentOffsetY > _scrollViewContentOffsetYThreshold) {
	    layer.position = CGPointMake(layer.position.x,
		                         22 - MIN((contentOffsetY - _scrollViewContentOffsetYThreshold), 48.0));
	}
	else
	{
	     layer.position = kNavBarDefaultPosition;  // then don't do any of this fancy scrolling stuff
	}
   }
}

@end

And really, that’s it. Now, technically the UINavigationBar hasn’t gone away, so you may want to disable user interaction when it’s not visible so that ‘phantom touches’ are not registered.

All the code is in place, but how do you use it? Well, in whatever subclass you make of the above, which also has a UIScrollView (or UITableView), you make sure that the delegate is set to the view controller, and you set some values for the _scrollViewContentOffsetYThreshold variable, and assign the _scroller pointer the object that will control the position of the UINavigationBar.

That’s it.

(UPDATE)  I recently got an email from a reader who had some trouble with this, and so I should also mention that when the UINavigationBar scrolls away, it’s best to have content underneath it!  This is accomplished by making sure that the view controller tells the navigation controller to set the toolbar to transparent

self.navigationController.toolbar.translucent = YES;  // also relevant could be the barStyle property

setting that property causes the UINavigationController to do a little magic and adjust the view frames of the viewControllers it is managing.

If you do this however, and you are using a TableView or UITableViewController, it would also make sense to play with your table view’s contentInset property. Namely, setting contentInset.top to 44 or the height of the navigation bar.

About these ads

12 thoughts on “UINavigationBar that scrolls away

  1. ha, I wish I would have googled this earlier. Awesome approach to this (silly) problem. Literally replaced a couple hours of replacing UINavigationBar functionality. Thanks for the post!

  2. Awesome work Stephen,

    Just wondering how you are dealing with pushing and popping view controllers? Say the nav bar is hidden and the user completes an action that pushes a view controller how can we make it so that the pushed view controller shows the nav bar but the pushing view controller remains hidden?

    Also is there anything we can do with the scrollViewIndicator to make it a little more accurate?

    • Hi Peter, glad you like it. To answer your question, have a look in the viewWillAppear method. That’s where the navBar gets reset to ‘default’ position. It makes use of the contentOffset.y property. On a newly pushed view controller, this will be 0. If you pop back to a controller who’s view has not been deallocated, then the contentOffset will be where you left it. I’d just say… try it out!

      The catch with this implementation is that the underlying view of this view controller tucks in underneath the UINavigationBar, so the UITableView you assign to _scroller, you can then have his scrollViewIndicators set to what you like (most likely +44 to the top property) in the viewWillAppear method. I did this on a project. Worked great. Didn’t look strange at all. Again, try it out!

      • Thanks for the further info!

        I have tried to get the pushing and popping to play ball but not having much luck, if I push with the navBar hidden then in viewWillAppear I set the navigation bars layer to CGPointMake(160, 44); it seems to show the nav bar for a quick second then hides. I am not seeing the nav bar to hidden or anything like that so I am pretty lost for ideas here?

      • Hmm, I guess I’d have to take a look at your code, because this recipe’s pretty straightforward… maybe you’re doing something a little funky that’s causing this to not work properly?

        You could also try setting some breakpoints to try to find out where this might be getting called unexpectedly by someone else.

    • did you make sure that the navigation bar is translucent? This way the underlying view that is used for the view controller will use the space underneath the navigation bar.

      you then have to adjust your scroll view’s contentInset property by 44pt.

      This whole technique *is a hack*, so there will be some aspects of it that won’t *feel* clean.

      • Thank you so much again. I have set
        self.navigationController.navigationBar.translucent = YES;
        and it worked. The only issue I am still facing is , when I open my view in landscape mode the
        – (void)scrollViewDidScroll:(UIScrollView*)aScrollView
        method is not getting called.Opening in landscape mode dosn’t give the scrolling behavior of navigation bar.But it is working perfectly when i launch the app in portrait mode and rotate iPad/iPhone onward. I am using webview embedded in uiscrollview.

  3. umm…. a UIWebView is a subclass of UIScrollView. You shouldn’t need to embed a UIWebView in a scrollview! As for your autorotate problem…. no idea. That’s not really related to this post.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s