Why this happens
A UINavigationController manages a hierarchy of child view controllers, but it owns only one shared instance of UINavigationBar. When you set isNavigationBarHidden = true or call setNavigationBarHidden(_:animated:), you are modifying the navigation controller itself, not an isolated property of your current view controller.
Setting self.navigationController?.isNavigationBarHidden = true inside viewDidLoad() causes two distinct issues. First, viewDidLoad() runs only once when the controller is initially instantiated. If a user pushes a second controller and then pops back to the first, viewDidLoad() will not fire again, leaving the bar in whatever state the second screen defined. Second, if you push another view controller onto the stack without explicitly restoring the bar, the container preserves its hidden state on all subsequent screens.
Failing to pass the animated flag during transitions also breaks interactive pop gestures. If an un-animated change occurs while the user drags from the screen edge to go back, the navigation bar can disappear abruptly or leave an empty space at the top of the interface.
How to fix it
1. Toggle visibility in lifecycle methods with animation
The standard approach in UIKit is pairing visibility updates inside viewWillAppear(_:) and viewWillDisappear(_:). Always pass the incoming animated argument to setNavigationBarHidden(_:animated:) so the navigation bar fades smoothly in coordination with the push or pop transition.
Add this to the view controller that needs a hidden navigation bar:
import UIKit
class RootViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}
}
When RootViewController appears, it hides the navigation bar. When the user navigates to any detail screen, viewWillDisappear runs and restores the bar for the incoming view controller.
2. Centralize bar visibility with UINavigationControllerDelegate
If you have a complex navigation flow with multiple screens that alternate between hidden and visible states, placing toggle logic inside every individual controller can become brittle. A canceled swipe-to-pop gesture can occasionally cause lifecycle timing desynchronizations.
You can handle navigation bar visibility centrally by adopting UINavigationControllerDelegate:
import UIKit
class MainNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func navigationController(
_ navigationController: UINavigationController,
willShow viewController: UIViewController,
animated: Bool
) {
// Determine visibility based on controller type or protocol conformance
let shouldHideBar = viewController is RootViewController
navigationController.setNavigationBarHidden(shouldHideBar, animated: animated)
}
}
This approach checks the destination controller immediately before the transition runs. It prevents code duplication across view controllers and ensures that the bar visibility correctly reflects the top view controller even after canceled gestures.
3. Handling SwiftUI views inside a UIHostingController
If your screen is built in SwiftUI and presented inside a UIKit UINavigationController via UIHostingController, configure toolbar visibility directly in the SwiftUI view body rather than toggling UIKit properties in a host wrapper:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("No Navigation Bar Here")
}
.toolbar(.hidden, for: .navigationBar)
}
}
SwiftUI automatically manages the presentation transition when pushing to another SwiftUI view where the navigation bar is visible.
How to prevent it
- Never alter
isNavigationBarHiddeninsideviewDidLoad(); reserve screen-level appearance changes forviewWillAppear(_:). - Always use
setNavigationBarHidden(_:animated:)and pass theanimatedparameter from the lifecycle callback. - Pair every hide call in
viewWillAppearwith a corresponding show call inviewWillDisappearon the same view controller. - For navigation flows containing multiple hidden and shown states, adopt
UINavigationControllerDelegateto manage visibility globally.
FAQ
Why does the navigation bar snap abruptly during a swipe-to-pop gesture?
This happens when you modify isNavigationBarHidden directly or pass animated: false to setNavigationBarHidden. Always pass the animated parameter provided by viewWillAppear or viewWillDisappear so UIKit coordinates the alpha animation with the gesture progress.
What happens if a user starts an interactive pop and cancels it?
When using setNavigationBarHidden(_:animated:) inside viewWillAppear and viewWillDisappear, UIKit automatically handles reversed transition animations if the interactive pop is canceled. If you experience visual desynchronization, move visibility control to navigationController(_:willShow:animated:) in a UINavigationControllerDelegate.
Should I hide the navigation bar if I only want to remove the back button?
No. If you only want to prevent navigation backwards or remove the default arrow, set navigationItem.setHidesBackButton(true, animated: false) instead of hiding the entire navigation bar.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
