Fixing UINavigation​Controller Push Animation Layout Issues on iOS 26

1 September 2025

Building apps that adapt gracefully across size classes and devices has for some time relied on UISplitViewController. However, in certain configurations on iOS 26, this can lead to undesirable animations when views are initially laid out when a view controller is pushed on to a navigation stack. In this article, we will discuss the root cause of the issue and how to resolve it.

The Problem

UISplitViewController has the concept of a compact column - this is the view controller that displays when the split view is collapsed, either because the app is running on an iPhone or on an iPad within a narrow multitasking window. In many apps, this scenario requires the use of a UITabBarController to manage the top level of the application's UX hierarchy.

Within each tab in the tab bar, it is not uncommon to have a UINavigationController to manage the flow in the particular tab. As such, our view controller hierarchy might look like this:

let navigationController = UINavigationController(rootViewController: MyViewController())
let tabBarController = UITabBarController()
tabBarController.viewControllers = [navigationController]
let splitViewController = UISplitViewController(style: .doubleColumn)
splitViewController.setViewController(tabBarController, for: .compact)

In this example, the split view controller has a tab bar controller in its compact column, and the tab bar controller has a single tab, containing a navigation controller. So far, so good. Let's see how this might look if the MyViewController at the root of the navigation controller shows a button at the top of the view:

A simple UI with a button inside a navigation controller, nested inside a tab bar controller and split view controller.

So far, so simple. Now let's see what happens if tapping that button pushes another one of our view controllers containing the button on to the navigation stack:

A new view controller being pushed on to the navigation stack.

Did you see it? Let's slow things down to get a better look:

A new view controller being pushed on to the navigation stack in slow motion.

Here we can clearly see the problem. The initial layout of our view is being caught in the navigation push animation, causing it to animate from its original frame (at an origin of 0,0 with a width and height of 0) to its final frame size that fills the available space. Now that we've identified the problem, we can try to come up with a solution.

Preventing the Initial Layout Animation

The most obvious place to start is where the animation is triggered, which is in -[UINavigationController pushViewController​:animated:]. We can subclass UINavigationController and override this method to try out some solutions:

final class LayoutForcingNavigationController: UINavigationController {
    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        viewController.view.layoutIfNeeded()
        super.pushViewController(viewController, animated: true)
    }
}

Here we force a layout of the view controller's view that is about to be pushed before the push is initiated. Let's see if this helps:

The view controller's view being laid out before being pushed.

This is better, but we're not quite there. The view being pushed is now correctly sized, but we're still seeing it's content animating down from the top of the screen alongside the push animation. This is because when we tell the view to layout it is not part of the view hierarchy, and so does not have the layout margins set that cause it's content not to underlap the navigation bar. Instead, let's lay out the view after the push has been triggered:

final class LayoutForcingNavigationController: UINavigationController {
    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        super.pushViewController(viewController, animated: true)
        viewController.view.layoutIfNeeded()
    }
}
The view controller's view being laid out after being pushed.

This is no better, because the layout still occurs before the layout margins have been set. We need to find a way to force a layout once the view has been installed in the view hierarchy but ignoring the animation context of the push transition. Once our call to the super class implementation of -[UINavigationController pushViewController​:animated:] has completed, we find that the navigation controller's transitionCoordinator property is populated:

(lldb) po transitionCoordinator
▿ Optional<UIViewControllerTransitionCoordinator>
  - some : <_UIViewControllerTransitionCoordinator: 0x600001701140>

We can use -[UIViewController​TransitionCoordinator animateAlongside​Transition:completion:] to manipulate the view being pushed as the transition beings:

final class LayoutForcingNavigationController: UINavigationController {
    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        super.pushViewController(viewController, animated: true)
        transitionCoordinator?.animate { _ in
            UIView.performWithoutAnimation {
                viewController.view.layoutIfNeeded()
            }
        }
    }
}

Here we are triggering a layout of the view being pushed, but do so within a call to +[UIView performWithoutAnimation:]'s closure, which escapes the transition's animation context. Let's see how this looks:

The view controller's view being laid out without animation.

Hurrah! Our view now appears how we expect - it is pushed on to the navigation stack already laid out with the correct frame and correct content insets. All we need to do is replace our creation of UINavigationController with an instance of LayoutForcing​NavigationController, and the layout animation problem is solved.

Bug Reporting

Of course, this behaviour seen in iOS 26 is a bug and should be fixed. Please duplicate FB19766298 in Feedback Assistant to help get this prioritised.