Lets's say, I have 3 view controllers: A, B, C, all embedded into a navigation controller. A and B have a navigation bar, C doesn't.
I have a custom interactive transition between B and C. Since I need my navigation bar to disappear on C, I implemented this function of UINavigationControllerDelegate:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if viewController is C {
navigationController.setNavigationBarHidden(true, animated: animated)
}
else {
navigationController.setNavigationBarHidden(false, animated: animated)
}
}
Everything works perfectly in an common scenario, when I only make push-pop transitions.
But when I cancel transition B->C by calling cancel() on my UIPercentDrivenInteractiveTransition, the navigation bar doesn't show up on B. Here I have to call setNavigationBarHidden(false, ...), but I haven't managed to find the correct place to do it.
If I call it in B's viewWillAppear, navigation bar appears, but looks really strange - it contains elements which the C would have if it had navigation bar. If I pop back to the A, it will blink for a moment with expected contents, but immediately after transition the A navigation bar gets replaced by the B navigation bar!
So, it seems like the navigation bars stack is somehow broken after B->C transition cancellation, it appears to be shifted relatively to viewcontrollers like that:
has
-----------------------------------------------
| ViewController | Navigation bar of |
-----------------------------------------------
| A | B |
-----------------------------------------------
| B | C |
-----------------------------------------------
So, my question is what's the correct place to call navigationController.setNavigationBarHidden(false, animated: true) in this case?
Well, I've managed to find an ugly hack to fix it by myself. Maybe someone in this world would find it helpful.
In my custom UIPercentDrivenInteractiveTransition I override cancel function like that:
class CustomTransitionManager: UIPercentDrivenInteractiveTransition {
/// Indicates transition direction. Must be set before each transition.
var forward: Bool = true
/// Current navigation controller used for transition. Must be set before transition starts
var nc: UINavigationController?
/**
* Hack #1 with UINavigationController here
*/
override func cancel() {
super.cancel()
if forward {
self.nc?.setNavigationBarHidden(false, animated: false)
}
self.nc?.setNavigationBarHidden(forward, animated: false)
}
}
In each of the view controllers (A, B, C) I make the following hack:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Hide and immediately show navigation bar: this will restore it's correct state
self.navigationController?.setNavigationBarHidden(true, animated: false)
self.navigationController?.setNavigationBarHidden(false, animated: true)
}
The best solution probably would be a modal fullscreen presentation of C, but in my case I am working with a project already having corrupted navigation hierarchy and I had no time to fix it properly. Basically, that's the reason why I faced this issue.
Related
I've noticed that, when using the long-press back button feature in iOS 14, any properties relating to UINavigationController's view controller stack (.viewControllers, .topViewController, etc.) seem incorrect. Specifically, the order is reversed.
Regarding the .viewControllers property, Apple's docs state:
The root view controller is at index 0 in the array, the back view controller is at index n-2, and the top controller is at index n-1, where n is the number of items in the array.
If I've got three view controllers in a nav stack like as follows
[ViewController01, ViewController02, ViewController03] and print out the .viewControllers property in viewWillAppear, I get the expected output of:
[ViewController01]
[ViewController01, ViewController02]
[ViewController01, ViewController02, ViewController03]
If I tap the back button from ViewController03, I get the expected output from viewWillAppear in ViewController02:
[ViewController01, ViewController02]
However, if I set everything up again so I've got [ViewController01, ViewController02, ViewController03] and then use the long-press back button feature to jump back to ViewController01, I get the unexpected output of:
[ViewController03, ViewController01]
From viewWillAppear in ViewController01.
I'm not expecting this because ViewController03 isn't, and never was, the root view controller of the navigation stack. As per the docs, I'm expecting:
[ViewController01, ViewController03]
Could someone please let me know if this is expected behaviour or if I've overlooked something super-obvious?
Thank you!
I've reproduced this in a small sample app based on a "single view controller" project. Just embed the initial view controller in a nav stack and include the following:
class StubViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("\(self) will appear. Current nav stack follows:")
print("\(self.navigationController?.viewControllers ?? [])")
}
}
class ViewController: StubViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.pushViewController(TestViewController01(), animated: true)
}
}
class TestViewController01: StubViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.pushViewController(TestViewController02(), animated: true)
}
}
class TestViewController02: StubViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.pushViewController(TestViewController03(), animated: true)
}
}
class TestViewController03: StubViewController {
}
(I'm aware the above is very horrible)
Basically, what's happening here is that you have accidentally looked inside the sausage factory and you've seen how the sausage is made. And it isn't pretty...!
The workaround is: don't do that. Give the view controllers stack a chance to settle down before you look at it:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
print("\(self) will appear. Current nav stack follows:")
print("\(self.navigationController?.viewControllers ?? [])")
}
}
Even more simple: just move the code to viewDidAppear.
i'm stuck with this too, but what i've found is navigation controller changed the viewControllers stack because of animation to handle what controller will goes to another in back operation
for example if stack is [vc1, vc2, vc3, vc4] and you are in vc4 and if you call
navigationController.popToRootViewController(animated: true)
stack will be [vc4, vc1] in willShow delegate method.
the only way i found is to set animation to false
navigationController.popToRootViewController(animated: false)
to keep the stack as what it really is
Been working with UIKit for years now. It's amazing how issues like this seem to pop-up out of the blue.
I have a simple navigation setup:
UINavigationController
HomeViewController [push]
DetailViewController [push]
ModalViewController [modal]
A root navigation controller with 2 children pushed onto the stack. Then a modal presented from the root nav controller.
For some reason, the following snippet of code isn't working as expected:
extension UINavigationController {
func popToViewController(_ vc: UIViewController, animated: Bool, completion: #escaping ([UIViewController]?)->()) {
let popped = popToViewController(viewController, animated: animated)
if let coordinator = self.transitionCoordinator {
coordinator.animate(alongsideTransition: nil) { _ in
completion(popped)
}
}
else {
completion(popped)
}
}
}
using the extension:
navigationController.popToViewController(
homeViewController,
animated: true
)
No errors, warnings or crashes occur. UI is still fully responsive. But the DetailViewController in the stack is never popped. Inspecting the extension's popped variable, results in an empty array - which makes sense as the DetailViewController is clearly not removed from the stack.
What could prevent a navigation controller from popping a valid vc off of it's stack?
Things I've checked:
homeViewController is in the stack already, and I'm asking it to pop to the same instance. i.e. navigationController.viewControllers.contains(homeViewController) == true
I'm on the main thread. i.e. Thread.isMainThread == true
navigationController.viewControllers returns the same array before & after calling popToViewController(_vc:animated:)
Manually figuring out what vc's need to be popped, and calling setViewControllers(_ vcs:animated:) with the vcs I want to keep (in this case, just the HomeViewController instance). This still has the same issue.
I want to say this has something to do with popping view controllers off the stack from behind a modal presentation. But, as far as I know this is an okay thing to do. Plus, I've done it before and have had no issues in the past.
I have a navigationcontroller with navigation flows as shown below:
NC -> A -> B
B appears through a push segue.
The navigationbar of A is made transparent using following
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController!.navigationBar.isTranslucent = true
self.navigationController!.navigationBar.setBackgroundImage(UIImage(), for: .default)
self.navigationController!.navigationBar.shadowImage = UIImage()
}
and is translucency is set to false in viewWillDisappear so that B can have the usual navigation bar:
override func viewWillDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
self.navigationController!.navigationBar.isTranslucent = false
}
The issue is that when Back button is pressed in B to return to A, The navigation bar of B appears momentarily before disappearing. How to solve this issue?
PS: I do not want to add code to overridden methods of B as B might be shared by other navigation controller.
The issue is that when Back button is pressed in B to return to A, The
navigation bar of B appears momentarily before disappearing. How to
solve this issue?
You do not need to toggle anything in your viewWillDisappear method. Just toggle everything in your viewWillAppear method in your every screens.
Is this what you want? If so, I made a sample project on Github just for you, and for other people who are new to iOS in the future.
https://github.com/glennposadas/showhidenavbar-ios
Though it uses my very simple cocoapod, you can just copy everything from my framework and sample project.
Consider a storyboard where we have UITabBarController, in it any UIViewController(lets call it VC) embedded in a UINavigationController. We want VC to have a BarButtonItems on its navigation bar. This storyboard is presented by push segue from another storyboard (having another navigation controller).
Everything looks OK in XCode, but navigation bar does not change in VC at the runtime. However when I change presenting this storyboard from push to modal, everything seems to be fine. IMHO it is because of embedding the navigation controller but I do not see any reason why it is not working. Any idea how to fix it legally (presenting by push) and without any pain would be helpful.
Thanks in advance
So I think you will have to employ some code to fix your issue but not much. I built a test project to test this and will attach images along with code.
First if I understand you correctly you have a navigationController push the new storyboard in question. See attached image.
I named the storyboard being pushed because that is what is happening. Then in my storyboard named Push here is the setup.
In the first view controller of the tabbarcontroller I added the below code. Obviously this hides the navigation controller that pushed us here. If you then visit controller number 2 our new navigation controller and items show. If hiding the navigation controller in the tabbarcontroller view controller 1 is not what you want to do then. continue reading.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
//or to unhide from returning the opposite ->self.parent?.navigationController?.isNavigationBarHidden = true
self.parent?.navigationController?.isNavigationBarHidden = true
}
If you did not want to hide the navigation controller in the first view controller but when visiting controller 2 you want to see your items then add this to your viewWillAppear and in the first controller in viewWillAppear change the code from true to false.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Do any additional setup after loading the view, typically from a nib.
self.parent?.navigationController?.isNavigationBarHidden = true
}
This hides the parent navigation controller as basically that was covering up your navigation controller in your example. So above hides the parent navigation controller. This is also why presenting modally worked. Your navigation controller was hidden from the start. Hope this helps.
**Edit
If you want the navigation controller in tab 2 view controller but you want to keep the parent in tab one to be able to go back with the back button you can set this in viewWillAppear instead so it would look like this in view controller 1.
//tabcontroller vc 1
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.isNavigationBarHidden = false
}
And in tabcontroller view controller 2 with the item in the bar you could do this.
//tabbarcontroller vc 2 with own navigationcontroller
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.parent?.navigationController?.isNavigationBarHidden = true
}
Finally if you want the back button visible in both controllers but want different right buttons do it programmatically in viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.navigationItem.setRightBarButton(UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(FirstViewController.editSomthing)), animated: true)
}
And if you want to remove it in the other controller
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.tabBarController?.navigationItem.rightBarButtonItem = nil;
}
In Both of the above examples directly above this, we are keeping the parent navigation controller so you would not need to embed your view controllers of the tab controller inside uinavigation controller.
You could also use a combo of the above code if you want the hide/show parent navigation controller in viewWillAppear as well. Some of this is dependent on the view hierarchy you choose now and in the future.
I have the hidesBottomBarWhenPushed = true set for one of my UIViewController's (call it ViewControllerA) that is pushed onto my UINavigationController stack. I also opt to show the bottomBar when I push a new ViewController ontop of ViewControllerA. Therefore I have:
class ViewControllerA: UIViewController {
override func viewWillDisappear(animated: Bool) {
self.hidesBottomBarWhenPushed = false
}
override func viewWillAppear(animated: Bool) {
self.hidesBottomBarWhenPushed = true
}
This all works fine.
When I push ViewControllerA, the bottom bar hides.
When I push any other ViewController, the bottom bar shows.
However, when I am traveling backwards in the navigation stack (aka hitting the UIBarButtonItemBack button), I cannot get the bottomBar to hide when I pop the navigation stack to reveal ViewControllerA.
What am I missing? Thanks!
Got it! Here's what worked:
class ViewControllerCustom: UIViewController {
init() {
self.hidesBottomBarWhenPushed = true
}
override func viewDidAppear(animated: Bool) {
self.hidesBottomBarWhenPushed = false
}
}
And then in every UIViewController's custom implementation of BarButtonItemBack pressed I check to see if the previous view controller (that will be popped to needs to hide the tab bar). Granted I abstracted this out into a general function so I didn't need to repeat code, but here's the concept. Thanks for the help figuring this out though!
func barButtonItemBackPressed(button: UIButton) {
var viewControllers = self.navigationController!.viewControllers as! [UIViewController]
if ((viewControllers[viewControllers.count - 2]).isKindOfClass(ViewControllerCustom.self)) {
(viewControllers[viewControllers.count - 2] as! ViewControllerCustom).hidesBottomBarWhenPushed = true
}
self.navigationController?.popViewControllerAnimated(true)
}
I believe the intended use of this property is to hide the bar when pushed. So, when your view controller appears after the top-most one is popped, it wasn't pushed on the stack, so it doesn't change the tab bar's appearance.
This leaves you with two options:
1) Keep the bottom bar for all view controllers. When text is being entered, the keyboard covers the bottom bar.
2) Hide the bottom bar for View Controller A, as well as any other view controller that is pushed on top of A.