iOS doesn't show tab bar after popping a view controller with hidesBottomBarWhenPushed sets to true

48 Views Asked by At

I have an issue with the hidesBottomBarWhenPushed.

It works correctly when pushed (hides tab bar) and when popped (shows tab bar). But I have this little gem:

  • There is a UITabBarController which has a UINavigationController as tab with in it a single UIViewController
  • Push a new UIViewController on the stack, this view controller has hidesBottomBarWhenPushed set to true in the init. The tab bar is hidden correctly
  • There is a button in this view that performs two actions
    • Present a new view controller modally (full screen, for sheet presentation this works)
    • Pop the current view controller
  • Dismiss the modally presented view
  • The initial UINavigationController is shown with its child, the tab bar is still hidden however

So sadly since there are no official functions to call on the UITabBarController to hide or show the UITabBar I don't see a good way to fix this. There is also no function to reevaluate the current state.

If the presentation is slightly delayed all works fine (but well there are some stakeholders who don't want that...)

So this is the complete code that reproduces the bug (assumes a new project with a storyboard where you have a UITabBarController as the entry point, in it a single UINavigationController with the ViewController as its child)

Please forgive the 'uglyness', it is just simple demo code

import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tabBarController?.tabBar.isTranslucent = false
        
        view.backgroundColor = .red
        
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.navigationController?.pushViewController(VC2(), animated: true)
        }
    }
}


class VC2: UIViewController {
    override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        
        hidesBottomBarWhenPushed = true
        
        navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: UIAction(handler: { _ in
            let nvc = self.navigationController
            let vc = UINavigationController(rootViewController: VC3())
            vc.modalPresentationStyle = .fullScreen
            
            // With a delay it all works fine
//            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                nvc?.present(vc, animated: true)
//            }

            self.navigationController?.popViewController(animated: true)
        }), menu: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .blue
    }
}

class VC3: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .green
        
        navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: UIAction(handler: { _ in
            self.dismiss(animated: true)
        }))
    }
}
2

There are 2 best solutions below

0
Saren Inden On

Seems that a 'hacky' solution is to make a modification to the 'owning' UINavigationController

override public func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    // Determine if we are in the invalid state
    guard
        let tabBarController,
        tabBarController.tabBar.isHidden,
        let last = children.last,
        !last.hidesBottomBarWhenPushed
    else {
        return
    }

    // Force the navigation controller to reevaluate the current `hidesBottomBarWhenPushed`
    pushViewController(UIViewController(), animated: false)
    popViewController(animated: false)

    // The safe space is shown but the tab bar is still set to hidden
    tabBarController.tabBar.isHidden = false
}

For me this works without any visual artifacts, but it is also a bit hacky which is always a questionable solution.

0
Saren Inden On

And I found another solution. This only works if the order is something like

vc.present(modalVC, animated: true)
...
self.navigationController?.popViewController(animated: false)

and not the other way around (so the present must be done before the pop)

var animatedPop = true

if let presentedViewController, presentedViewController.modalPresentationStyle == .fullScreen {
    animatedPop = false
}

navigationController?.popViewController(animated: animatedPop)

Apparently for some magical reason the system does evaluate the hidesBottomBarWhenPushed correctly if not animated