Question about iOS 14 navigation stack management (different from iOS 13)

Viewed 2664

I've been testing our app on iOS and I've noticed a weird behaviour that happens on iOS 14, but it doesn't happen on iOS 13. It's regarding the navigation stack and how it works.

Say I have a navigation stack of [A, B, C], where C is of course the topmost view controller. I have a button in the C view controller which gets the navigation stack, removes C and then B, and finally it adds a new view controller D. Then I set the new stack to the navigation controller.

On iOS 13, the resulting stack is [A, D]. On iOS 14, the resulting stack is [C, A, D]. Somehow, it didn't let me remove the current view controller from the stack, but it did add to the bottom of the stack.

My only explanation has been that it has something to do with the new navigation history feature in iOS 14 (long hold on the Back button). Somehow this introduced some new constraints on the management of the navigation stack.

Hopefully someone can help me understand the new behaviour.

4 Answers

I'm facing a similar issue in the latest iOS 14 as well - If I use setViewControllers:animated: on the UINavigationController, instead of replacing the nav stack entirely with the array of view controllers provided as an argument, it retains the current top most view controller, and pushes the new view controllers on top of it.

Basically, if your current view controller stack is:

[V1, V2, V3,... Vn]

and you use setViewControllers:animated with an argument of:

[X1, X2, X3,... Xn]

After the transition, your view controller stack will be:

[Vn, X1, X2, X3,... Xn]

This bug does not happen on iOS 13 and lower versions.

It appears this was indeed an early beta issue and has been fixed in iOS 14 beta 6 / Xcode 12 beta 6.

let navigationController = UINavigationController(rootViewController: originalViewController)

navigationController.setViewControllers([replacementViewController], animated: true)

Inspecting the view controllers after calling setViewControllers

navigationController.viewControllers 

Now yields the expected results [replacementViewController] (matching the iOS 13 behavior).

I found what may be a simple workaround to this issue. If you are going to use setViewControllers to set a new array of VC's, first call setViewControllers with an empty array (UIViewController), then immediately setViewControllers again with your actual array.

One caveat of this is that the willShow navigationController delegate will get called and it's viewController argument will be unitialized. So you can first check to see if the viewControllers.count is 0 and if so exit the will show.

I also had this kind of issue. On my case I got a navigation controller with only a view controller on its stack, say [viewControllerA]. And when doing:

navigationController.setViewController([viewControllerB], animated: true)

on iOS 14 the navigation stack becomes:

[viewControllerA, viewControllerB]

while on iOS 13 (and below) it becomes:

[viewControllerB]

Like you can see on the Apple foruns thread referred above by @Sanzio Angeli, this is caused by a different behaviour when setViewControllers is called with true on animated parameter.

That behaviour is not trivial and despite being describe in detail on the method's documentation, if you check its source you can read this comment:

open func setViewControllers(_ viewControllers: [UIViewController], animated: Bool) // If animated is YES, then simulate a push or pop depending on whether the new top view controller was previously in the stack.

SOLUTION:

a) Set animated parameter to false, calling:

navigationController.setViewController([viewControllerB], animated: false)

or

b) Simply set viewControllers this way (which is identical to previous call):

navigationController.viewControllers = [viewControllerB]
Related