34

I'm trying to use the new feature added in iOS 8 - hiding the navigation bar while user is scrolling the table view (similar to what mobile Safari does). I'm setting the property hidesBarsOnSwipe of UINavigationController to YES in viewDidAppear method of UITableViewController:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    if([self.navigationController respondsToSelector:@selector(hidesBarsOnSwipe)]) {
        self.navigationController.hidesBarsOnSwipe = YES;
    }
}

The navigation bar hides when the view is being scrolled. So far so good. But the status bar is still visible and my table view contents show through it, which looks ugly:

enter image description here

I tried setting edgesForExtendedLayout to UIEdgeRectNone or adjusting the contentInset of the table view, but it didn't help. Is there any other solution to hide the status bar along with the navigation bar, or make it opaque?

Michał Ciuba
  • 7,876
  • 2
  • 33
  • 59

9 Answers9

40

Actually it is pretty easy to do. You just need to connect navigation isNavigationBarHidden property with status bar.

Objective-C

- (BOOL)prefersStatusBarHidden {
    return self.navigationController.isNavigationBarHidden;
}

Swift <= 2.3

override func prefersStatusBarHidden() -> Bool {
    return navigationController?.navigationBarHidden ?? false
}

Swift 3.0

override var prefersStatusBarHidden: Bool {
    return navigationController?.isNavigationBarHidden ?? false
}

And be sure you have "View controller-based status bar appearance" = "YES" in your application .plist file.

iOSergey
  • 636
  • 5
  • 5
30

Building off of anas' answer, I have a working solution (I'm assuming tableViewController is your UITableViewController instance):

In a UINavigationController subclass (or also potentially from tableViewController):

- (void)viewDidLoad {
    if ([self respondsToSelector:@selector(barHideOnSwipeGestureRecognizer)]) {
        // iOS 8+
        self.hidesBarsOnSwipe = YES;
        [self.barHideOnSwipeGestureRecognizer addTarget:self action:@selector(swipe:)];
    }
}

- (void)swipe:(UISwipeGestureRecognizer *)recognizer {
    BOOL shouldHideStatusBar = self.navigationController.navigationBar.frame.origin.y < 0;
    tableViewController.hideStatusBar = shouldHideStatusBar;
    [UIView animateWithDuration:0.2 animations:^{
        [tableViewController setNeedsStatusBarAppearanceUpdate];
    }];
}

In your tableViewController:

@property(nonatomic, getter = shouldHideStatusBar) BOOL hideStatusBar;

- (BOOL)prefersStatusBarHidden {
    return [self shouldHideStatusBar];
}

Let me know if this doesn't work for you. A few non-obvious things:

  • self.navigationController.navigationBar.frame.origin.y was -44 (the negative height of the navigation bar) when hidden, and 20 (the height of the status bar) when visible. There was no in-between, even during animations, so a negative value == hidden and a nonnegative value == visible.
  • The child view controller is the one queried for whether or not the status bar should be hidden. In my case, I have a UIViewController within a UINavigationController within a UITabBarController, and it didn't work until I overrode prefersStatusBarHidden on the UIViewController.
  • Since a hidden status bar has no frame, your content might jerk upwards 20 points unless you wrap the call to setNeedsStatusBarAppearanceUpdate in an animation block.
  • Hopefully the syntax is correct; I backported this from my Swift code.
Andrew
  • 2,438
  • 1
  • 19
  • 19
  • Yeah, that works. Thanks! Anyway the end result is still not satisfying to me for a different reason. To show the navigation bar again (after it was hidden) one has to swipe down quite quickly. Simply scrolling the table view to the top won't work. I think it might be confusing to the user. Maybe I'm doing something wrong, or someone has an idea on how to deal with it? – Michał Ciuba Sep 22 '14 at 15:42
  • Area you talking about tapping the status bar to scroll to the top? Hmm... Maybe you can try setting the `UIScrollViewDelegate` on the table view and animate the navbar back on-screen? Maybe you can hook into `scrollViewDidScrollToTop` and check the `origin.y` of the navbar? Maybe? – Andrew Sep 22 '14 at 15:52
  • No, I meant regular scrolling but with a low velocity (such low that it won't be recognized as a 'swipe down' gesture). Nevermind, I checked mobile Safari and it works the same. So it's supposed to work that way and not how I expected. – Michał Ciuba Sep 22 '14 at 15:56
  • Yeah that was my other thought, I guess it's just a new convention that nobody's really used to yet since there aren't a lot of iOS 8 apps around! – Andrew Sep 22 '14 at 16:09
  • I just released this little class which closely Mimics Safari's swipe-to-hide behavior https://github.com/appfigures/AFSwipeToHide – oztune Oct 30 '14 at 20:43
  • This answer is wrong, all of this is automatically handled by UINavigationController, all you need to do is define prefersStatusBarHidden and return self.navigationController.navigationBarHidden – pronebird Feb 20 '15 at 12:54
8

That new property comes with its barHideOnSwipeGestureRecognizer.

From the UINavigationController Class Reference:

You can make changes to the gesture recognizer as needed but must not change its delegate and you must not remove the default target object and action that come configured with it. Do not try to replace this gesture recognizer by overriding the property.

But you can add a target:

[self.navigationController setHidesBarsOnSwipe:YES];
[self.navigationController.barHideOnSwipeGestureRecognizer addTarget:self action:@selector(swipeGesture:)];

... and do whatever you want in the callback:

- (void)swipeGesture:(UIPanGestureRecognizer*)gesture
{
    // Tweak the status bar
}

You might have to manually switch on the gesture states, figure out when to hide/show the status bar, etc. Hope that helps!

Anas
  • 866
  • 1
  • 13
  • 23
  • do you know why I get a crash when trying to swipe after leaving view controller and loading it again? It works the first time, but then I get bad access error when I try it again. – pasevin Jan 09 '15 at 00:26
  • @pasevin Hard to say without seeing the crash log – Anas Jan 12 '15 at 17:28
  • This way of doing it is awesome. But I cannot find a way to know if the gesture is *showing* or *hiding* the Navigation bar, any idea of how to find this information in the UIPanGestureRecognizer ? – Thomas Besnehard Dec 09 '16 at 16:50
3

The answer from @iOSergey works perfect!

Here is the solution in Swift 1.2. Add the following code to the views .swift file:

override func prefersStatusBarHidden() -> Bool {

    return self.navigationController!.navigationBarHidden as Bool

}
3

If you want to hide status bar with animation:

override func preferredStatusBarUpdateAnimation() -> UIStatusBarAnimation {
    return .Slide
}

override func prefersStatusBarHidden() -> Bool {
    return navigationController?.navigationBarHidden ?? false
}
Tai Le
  • 8,530
  • 5
  • 41
  • 34
2

After much struggle, I was finally able to solve this.

  1. Change TableViewController to UIViewController
  2. Drag a TableView to the UIViewController and align it right underneath the navigation bar in the main.storyboard
  3. Add missing constraints.
  4. click on tableview and inspect the constraints
  5. set bottom space to: superview to 0
  6. set trailing space to superview to 0
  7. set leading space to superview to 0
  8. set "Align Top to: Top Layout Guide.Top to 0 (Very Important)

Add the following code to the UIViewController's viewDidLoad function:

// Create a solid color background for the status bar (in Swift Code)

let statusFrame = CGRectMake(0.0, 0, self.view.bounds.size.width,
UIApplication.sharedApplication().statusBarFrame.size.height)

var statusBar = UIView(frame: statusFrame)

statusBar.backgroundColor = UIColor.whiteColor()

self.view.addSubview(statusBar)

What you are doing is creating a solid bar right beneath the navigation bar. As the navigation bar moves up, the solid bar move up too and right behind the status bar.

The only problem is that when you rotate the screen side ways, the white bar still there even though the status bar disappears.

John Chen
  • 21
  • 2
1

Okay I spent all day doing this, hopefully this helps some people out. There's a barHideOnSwipeGestureRecognizer. So you could make a listener for the corresponding UIPanGesture, noting that if the navigation bar is hidden then its y origin is -44.0; otherwise, it's 0 (not 20 because we hid the status bar!).

In your view controller (swift 2):

 // Declare at beginning
var curFramePosition: Double!
var showStatusBar: Bool = true
self.navigationController?.barHideOnSwipeGestureRecognizer.addTarget(self, action: "didSwipe:")

...

override func viewDidLoad(){
    self.navigationController?.hidesBarsOnSwipe = true
  curFramePosition = 0.0 // Not hidden
  self.navigationController?.barHideOnSwipeGestureRecognizer.addTarget(self, action: "didSwipe:")
  ...
}

func didSwipe(swipe: UIPanGestureRecognizer){
    // Visible to hidden
    if curFramePosition == 0 && self.navigationController?.navigationBar.frame.origin.y == -44 {
        curFramePosition = -44
        showStatusBar = false
        prefersStatusBarHidden()
        setNeedsStatusBarAppearanceUpdate()
    }
    // Hidden to visible
    else if curFramePosition == -44 && self.navigationController?.navigationBar.frame.origin.y == 0 {
        curFramePosition = 0
        showStatusBar = true
        prefersStatusBarHidden()
        setNeedsStatusBarAppearanceUpdate()
    }
}

override func prefersStatusBarHidden() -> Bool {
    if showStatusBar{
        return false
    }
    return true
}
goodcow
  • 4,495
  • 6
  • 33
  • 52
0

Another way to do it is just add another view (above the tableview or collectionview or webview or scrollview or whatever) and set the view's top constraint to "Superview.Top" and its bottom constraint to "Top Layout Guide.Bottom" ,set the view's background color and thats it , you can even do it all in Interface Builder without any code. And if you want to respond to that event you can add a keypath observer to the view's bounds change , or subclass the view and override its bounds setter...

Oleg Sherman
  • 2,772
  • 1
  • 21
  • 20
0

You need to connect navigation isNavigationBarHidden property with status bar.

    return self.navigationController.isNavigationBarHidden;
Abhinandan Pratap
  • 2,142
  • 1
  • 18
  • 39