40

In my iOS app, my window's rootViewController is a tab bar controller with the a hierarchy like this:

  • UITabBarController
    • UINavigationController 1
      • FirstContentController
    • UINavigationController 2
      • ...
    • UINavigationController 3
      • ...
    • ...

When the user taps a certain row on FirstContentController, an instance of SecondController will be pushed onto its navigation controller. SecondContentController sets hidesBottomBarWhenPushed to YES in its init method and sets self.navigationController.toolbarHidden to NO in viewWillAppear:.

In iOS 6, the user would tap the row in FirstController and SecondController would get pushed onto the nav controller. Because it has hidesBottomBarWhenPushed set, it would hide the tab bar and, by the time the transition animation was complete, SecondController would be on the screen with its toolbar visible.

However, when testing this under iOS 7, hidesBottomBarWhenPushed's behavior seems to have changed. What I see now is:

  • the tab bar hides, as expected
  • the toolbar appears, as expected
  • a gap of unusable space exactly 49 pixels tall (the height of the tab bar) appears between the toolbar and the content view

The gap is completely unusable - it doesn't respond to touches and if i set clipsToBounds to YES on the main view, nothing draws there. After a lot of debugging and examining subview hierarchies, it looks like iOS's autosizing mechanism resizes the view controller's view to a height of 411 (on the iPhone 5). It should be 460 to reach all the way down to the toolbar, but the layout system seems to be including a "ghost" 49-pixel-tall tab bar.

This problem only occurs if the view controller has a tab bar controller as one if its parent containers.

On iOS 7, how can I have the tab bar disappear and a toolbar seamlessly slide into place when a new controller is pushed, and still have the view take up the entire space between the navigation item and the toolbar?

UPDATE

After further investigation, this only happens if SecondController's edgesForExtendedLayout is set to UIRectEdgeNone. However, unless I set that property to UIRectEdgeNone, the view's frame is too long and extends under the toolbar, where it can't be seen or interacted with.

Bill
  • 44,502
  • 24
  • 122
  • 213

14 Answers14

21

I found that adding the following 2 lines of code in viewDidLoad of SecondViewController (where you want to hide TabBar but show the tool bar) fixes the problem.

self.extendedLayoutIncludesOpaqueBars = YES;
self.edgesForExtendedLayout = UIRectEdgeBottom;

My viewDidLoad of SecondViewController is as follows:

- (void)viewDidLoad {
    [super viewDidLoad];
    // These 2 lines made the difference
    self.extendedLayoutIncludesOpaqueBars = YES;
    self.edgesForExtendedLayout = UIRectEdgeBottom;

    // The usual configuration
    self.navigationController.navigationBar.barStyle = UIBarStyleBlack;
    self.navigationController.navigationBar.translucent = NO;

    self.navigationController.toolbarHidden = NO;
    self.navigationController.toolbar.barStyle = UIBarStyleBlack;
    self.navigationController.toolbar.translucent = NO;
    .
    .
}

But you need to fix the frame of the view manually as this causes the size to be (320x504). Which means it extends even behind the tool bar. If this is not a concern for you then this solution should work.

srik
  • 1,447
  • 2
  • 20
  • 33
  • 1
    This should be voted on more. self.extendedLayoutIncludesOpaqueBars = YES; helped me. – Dvole Oct 15 '14 at 18:12
  • Thanks, this is very helpful - making the bar opaque does seem to work around the "dead zone" bug – Bill Oct 29 '14 at 11:45
18

You will not like this answer This is not the answer you want, but after some research on hiding the tab bar in iOS7, my conclusion is: don't!

Tab bars have never been meant to be hidden - after all why have a UITabBarController if you want to hide the tab bar. The hidesBottomBarWhenPushed on view controllers is for hiding the bottom bar of a navigation controller, not tab bars. From the documentation:

A view controller added as a child of a navigation controller can display an optional toolbar at the bottom of the screen. The value of this property on the topmost view controller determines whether the toolbar is visible. If the value of this property is YES, the toolbar is hidden. If the value of this property is NO, the bar is visible.

Moreover, you are warned not to modify the tab bar object directly. Again, from the documentation:

You should never attempt to manipulate the UITabBar object itself stored in this property.

This is exactly what you are doing when setting it to hidden.

In iOS6 this has worked, but now in iOS7, it doesn't. And it seems very error prone to hide it. When you finally manage to hide it, if the app goes to the background and returns, Apple's layout logic overrides your changes.

My suggestion is to display your data modally. In iOS7 you can create custom transitions, so if it is important to you to have a push transition, you can recreate it yourself, although this is a bit over the top. Normal modal transition is something users are familiar, and actually fits this case better than push which hides the tab bar.


Another solution is to use a toolbar instead of a tab bar. If you use the navigation controller's toolbar for your tabs, you can then use hidesBottomBarWhenPushed as you require and it would give you the behavior you expect.

Léo Natan
  • 56,823
  • 9
  • 150
  • 195
  • 2
    + 1 I totally agree with you, but I guess it *is* possible since the Facebook app makes use of it (e.g. when you click on a link and it opens in Facebook's built-in browser (which gets pushed). – HAS Dec 17 '13 at 15:55
  • 1
    @HAS Facebook app is using a custom toolbar rather than a tab bar. Then it is exactly the behavior `hidesBottomBarWhenPushed` provides. – Léo Natan Dec 17 '13 at 18:00
  • how about a custom toolbar ? – Radu Dec 23 '13 at 14:18
  • I agree that it is discouraged. Nonetheless, **it is possible with just one click**. See my answer below: http://stackoverflow.com/a/23570410/1766216 – Burny May 09 '14 at 17:13
8

Uncheck "Hide bottoms bars on push" and set your autoconstraints as if there is a tab bar. Then in "ViewDidLoad" of the controller you want to hide the system tab bar, put the following code.

[self.tabBarController.tabBar setFrame:CGRectZero];

This makes sure the tab bar still accepts user interaction yet not visible to users. (other alternatives such as setting it 0 alpha or hidden will render tab bar useless) Now the autoconstaraints will make sure your view displays correctly with the tab bar height as zero.

Léo Natan
  • 56,823
  • 9
  • 150
  • 195
Ah Ryun Moon
  • 370
  • 5
  • 4
  • Brilliant! This does the trick. Do you know of a way to restore the tab bar to its default size once the user pops the controller that hides it? – Bill Jun 26 '14 at 13:31
  • What happens if you rotate the device or minimize app and open it again? – Léo Natan Jun 26 '14 at 17:09
  • 1
    @Bill If you want to restore the height and width of tab bar at later time, I'd say, instead of making the frame CGRectZero, you set the y-value of frame origin to be the height of its containing view (so the tab bar is hidden and auto-constraint takes care of laying out the rest of views correctly). Then when you need it to appear again, set the y-value back to its original value. I haven't tried it but I'm guessing it should work fine for your purpose. – Ah Ryun Moon Jun 26 '14 at 23:07
  • In my application, I am seeking to temporarily swap the tab bar for a tool bar. This answer works great for me. The one place it breaks is on iPhone X, and only in one particular case: if I present a modal NavVC, dismiss it, and then attempt to swap the tab bar for tool bar, I get a space (showing the underlying table content) between the bottom of the screen and the tool bar, instead of it being flush to the bottom of the screen like it should be. Anyone run into this issue? – T'Pol Jan 20 '18 at 23:03
  • In case it's of help, [this solution](https://stackoverflow.com/questions/46232929/why-page-push-animation-tabbar-moving-up-in-the-iphone-x/47225653#47225653) helped me resolve my issue. – T'Pol Jan 21 '18 at 18:08
5

It's a bug in iOS 7 UIKit due to this particular combination of:

  • UITabBarController
  • hidesBottomBarWhenPushed = YES
  • edgesForExtendedLayout = UIRectEdgeNone
  • UINavigationController toolbar

You should file a bug with Apple and include your sample code.

To work around the bug you need to remove one of those four conditions. Two likely options:

  1. Fix the layout of your "second" view controller so that it works correctly when edgesForExtendedLayout is set to UIRectEdgeAll. This could be as simple as setting the contentInset on a scroll view.

  2. Don't use UINavigationController's built-in toolbar. Instead, create a separate UIToolBar instance and manually add it to your second view controller's view.

Darren
  • 25,520
  • 5
  • 61
  • 71
  • Thanks for the reply. I filed a radar a few days back - no answer yet. I think you're correct that it's a bug. That's a great suggestion to try changing contentInset - let me give that a shot. – Bill Dec 19 '13 at 20:53
  • 1
    Also, after hiding the tab bar, if iOS triggers a layout, your view will be messed up again. In shirt, a big mess. – Léo Natan Dec 20 '13 at 06:55
2

You do have to set the tabBar of the TabBarController to hidden and your view should have autosizing set to flexible height.

With this code it's working:

@implementation SecondController

-(id)init
{
    if( (self = [super init]) )
    {
    }

    return self;
}

- (void)viewDidLoad;
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight;
    self.tabBarController.tabBar.hidden = YES;
}

-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    // will log a height of 411, instead of the desired 460
    NSLog(@"frame: %@", NSStringFromCGRect(self.view.frame));
}

@end

Or, if you do want to use the hidesBottomBarWhenPushed method, you have to do this before you push the view controller obviously:

-(void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath
{
    SecondController* controller = [[SecondController alloc] init];
    controller.hidesBottomBarWhenPushed = YES;

    [self.navigationController pushViewController:controller animated:YES];
}

If using the second method, your viewDidLoad method can get rid of flexible height method as well as tabBarHidden:

- (void)viewDidLoad;
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    self.edgesForExtendedLayout = UIRectEdgeNone;
}

See the result:

enter image description here

Alexander
  • 7,178
  • 8
  • 45
  • 75
  • No, that doesn't work for me. Instead, the 49-pixel gap is between the bottom of the toolbar and the bottom of the screen. Also, you're not including the `self.edgesForExtendedLayout = UIRectEdgeNone`. – Bill Dec 16 '13 at 16:36
  • I've edited my answer and added the edgesForExtendedLayout - it is still working properly. Did you really use my implementation? Delete your viewWill / viewDidAppear completely and use my viewDidLoad as well as the didSelectRowAtIndexPath – Alexander Dec 16 '13 at 16:40
  • You're not displaying the toolbar. Add ` self.navigationController.toolbarHidden = NO` to viewWillAppear: or viewDidLoad and you'll see the problem. – Bill Dec 16 '13 at 18:16
  • The problem is the combination of a tab bar, a tool bar and UIRectEdgeNone self.edgesForExtendedLayout. I can get it to work with any two of those conditions, but not with all three. – Bill Dec 16 '13 at 18:20
  • It works great! But **self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight;** is not necessary. – adnako Mar 14 '16 at 14:56
2

The key to this conundrum is that the navigationcontroller.view.frame size doesn't change. Going of batkin's Gist here is a gist of my own.

FirstViewController.m

#import "FirstController.h"
#import "SecondController.h"

@implementation FirstController

-(id)init
{
    if( (self = [super init]) )
    {
        self.tabBarItem.title = @"Foo";
        self.tabBarItem.image = [UIImage imageNamed:@"Tab Icon.png"];
    }

    return self;
}

-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
    return 1;
}

-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
    UITableViewCell* cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];

    cell.textLabel.text = @"Click";

    return cell;
}

-(void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath
{
    SecondController* controller = [[SecondController alloc] init];
    self.tabBarController.tabBar.hidden = YES;
    [self.navigationController pushViewController:controller animated:YES];
}

@end

SecondViewController.m

#import "SecondController.h"

@implementation SecondController

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    self.view.backgroundColor = [UIColor redColor];
    self.view.clipsToBounds = YES;

    /* ENTER VORTEX OF DESPAIR */

    // without this, there's no gap, but the view continues under the tool 
    // bar; with it, I get the 49-pixel gap thats making my life miserable

    self.edgesForExtendedLayout = UIRectEdgeNone;

    //this resizes the navigation controller to fill the void left by the tab bar.
    CGRect newFrame = self.navigationController.view.frame;
    newFrame.size.height = newFrame.size.height + 49;
    self.navigationController.view.frame = newFrame;

    /* EXIT VORTEX OF DESPAIR */

    self.navigationController.toolbarItems = @[
                                               [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:nil action:nil]
                                               ];


}

-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    self.navigationController.toolbarHidden = NO;

    // will log a height of 411, instead of the desired 460
    NSLog(@"frame: %@", NSStringFromCGRect(self.view.frame));
    NSLog(@"frame: %@", NSStringFromCGRect(self.navigationController.view.frame));
}

-(void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.tabBarController.tabBar.hidden = NO;
    self.navigationController.toolbarHidden = YES;

    //this resizes the navigation controller back to normal.
    CGRect newFrame = self.navigationController.view.frame;
    newFrame.size.height = newFrame.size.height - 49;
    self.navigationController.view.frame = newFrame;

    //this is optional and resizes the view to fill the void left by the missing toolbar.
    CGRect newViewFrame = self.view.frame;
    newViewFrame.size.height = newViewFrame.size.height + 49;
    self.view.frame = newViewFrame;

}

@end
Hackmodford
  • 3,901
  • 4
  • 35
  • 78
2

If you are using Auto Layout,make sure you pin the view to its superview instead of Top Layout Guide or Bottom Layout Guide.

tounaobun
  • 14,570
  • 9
  • 53
  • 75
0

Have you tried to move your call hidesBottomBarWhenPushed in the viewDidLoad or before the secondViewController is pushed?

With ios7, a lot of timing issues appear if you don't do the calls at teh good moment.

Geraud.ch
  • 1,499
  • 10
  • 15
0

You mention that you can fix this by not touching the edgesForExtendedLayout. Is there a necessary reason that the content/controls of the view controller are contained in the root view of the pushed view controller? You might consider wrapping everything in a view that is the first and only child of the main view. Then adjust that view's frame in the viewDidLayoutSubviews of the pushed view controller to avoid having content permanently beneath the toolbar using the top/bottomLayoutGuide of the view controller.

Felix Lamouroux
  • 7,414
  • 29
  • 46
  • In my actual application, the main content view is added as a subview of the `UIViewController#view`. I set its frame manually in `loadView`, which works. But it's contained within the controller's view, which gets cut off at 411 pixels. So any taps made between the bottom of the controller's view and the toolbar don't make it to my subview, even though the subview's frame size is set correctly and stays correct even after the autosizing process takes place. – Bill Dec 16 '13 at 16:16
0

I built a new project using your Gist, and I encased the UITabBarController in a UINavigationController:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];

    UITabBarController* tabController = [[UITabBarController alloc] init];

    tabController.viewControllers = @[
                                      [[UINavigationController alloc] initWithRootViewController:[[FirstViewController alloc] init]],
                                      [[UINavigationController alloc] initWithRootViewController:[[FirstViewController alloc] init]]
                                      ];

    UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:tabController];
    [navController setNavigationBarHidden:YES];
    self.window.rootViewController = navController;

    return YES;
}

And to show the SecondViewController, here is what I did:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    SecondViewController* controller = [[SecondViewController alloc] init];

    // Reaching the UITabBarViewController's parent navigationController
    [self.parentViewController.navigationController pushViewController:controller animated:YES];
}

Finally, in the secondViewController:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    self.view.backgroundColor = [UIColor redColor];
    self.view.clipsToBounds = YES;

    // The following line only works in iOS7
    if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1) {
        self.edgesForExtendedLayout = UIRectEdgeNone;
    }

    [self.navigationItem setRightBarButtonItem:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:nil action:nil]];

    UIBarButtonItem * logoutButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemReply target:nil action:nil];
    NSMutableArray * arr = [NSMutableArray arrayWithObjects:logoutButton, nil];
    [self setToolbarItems:arr animated:YES];


    [self.navigationController setNavigationBarHidden:NO animated:YES];
    [self.navigationController setToolbarHidden:NO animated:YES];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [self.navigationController setNavigationBarHidden:YES animated:YES];
    [self.navigationController setToolbarHidden:YES animated:YES];
}

Here's what it does look: 3rd tentative

EDIT: Changed the example and changed the screenshot. Made the example iOS6 compatible.

Rufel
  • 2,630
  • 17
  • 15
  • Yes, but having the toolbar is essential – Bill Dec 17 '13 at 00:04
  • By toolbar, you mean some navigation buttons? I edited my answer to provide a "Save" button in the SecondController and updated the screenshot. Is that what you were looking to? – Rufel Dec 17 '13 at 02:14
  • No, there are four or five buttons in the toolbar to manipulate the selection in the main view. – Bill Dec 17 '13 at 02:28
  • The essence of the problem is that I need to hide the tab bar, display the toolbar and not have the content run under the toolbar. – Bill Dec 17 '13 at 02:45
  • 1
    @Bill Ok I get it! Take a look at my updated example. What I did is adding the UITabBarController in a UINavigationController, then using that NavigationController to push the SecondViewController. :) – Rufel Dec 17 '13 at 03:07
  • @Bill Did you try the updated example with the toolbar? I just corrected a few lines to make it works flawlessly in iOS6/7. – Rufel Dec 17 '13 at 15:36
  • Just tried it. I don't see a nav bar at the top; also, I don't think you're supposed to add a tab bar controller inside a nav bar controller. – Bill Dec 17 '13 at 16:51
  • @Bill: It does work for me. Here is a Gist with the minimal code required to make it work: https://gist.github.com/simondec/156386931767142e5859. UITabBarController inherits from UIViewController and in so, can be used like any other UIViewController (see doc here: https://developer.apple.com/library/ios/documentation/uikit/reference/UITabBarController_Class/Reference/Reference.html) – Rufel Dec 17 '13 at 17:03
  • you see a nav bar on top and a toolbar on the bottom for SecondController? Tab controllers are not allowed inside navigation controllers: https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/ViewControllerCatalog/Chapters/NavigationControllers.html#//apple_ref/doc/uid/TP40011313-CH2-SW27 – Bill Dec 17 '13 at 17:21
  • Did you get the code from my Gist? Yeah, I have a toolbar at the bottom and a navbar on top of the SecondViewController. Although it is not *recommended* to add a UITabBarController as a child of a UINavigationController, this is probably the cleanest way to handle your special case since a tab bar is not meant to be hidden neither in a UITabBarController. – Rufel Dec 17 '13 at 17:44
0

I manually manage hide/unhide of bottom-tab-bar along with fade animation by

 ...

[self.tabBarController.tabBar setHidden:NO];

[self.tabBarController.tabBar setAlpha:0.1];
[UIView animateWithDuration:0.2 animations:^{
    [self.tabBarController.tabBar setAlpha:1.0];
}];
 ...

Bottom Toolbar on SecondVC was added in IB. No problem so far. Using Storyboard.

dklt
  • 1,703
  • 1
  • 12
  • 12
0

I think you can set SecondController's edgesForExtendedLayout to UIRectEdgeBottom.

jianpx
  • 3,190
  • 1
  • 30
  • 26
0

This helps me: Choose you view controller in storyboard -> Go to properties -> Uncheck "Adjust Scroll View Insets"

George
  • 756
  • 1
  • 9
  • 16
-1

As @Leo Natan is pointing out, it seems as if hiding the tab bar and showing a toolbar is discouraged. Nevertheless, there is a very easy solution that is working:

Just check "Under Opaque Bars" in the view controller properties in the storyboard as shown below:

Burny
  • 64
  • 4
  • It's not that simple. If the SecondController then pushes another controller, I have a tabbar-sized region right above the toolbar that doesn't accept user interaction. – Bill Aug 28 '14 at 21:18