uikitipados

Layout constraint error when using a UINavigationController with UITabBarController on iPadOS


I have a Xcode 16.3 Swift/UIKit project that does programmtically setup a UITabBarController in SceneDelegate like so:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = (scene as? UIWindowScene) else { return }

    window = UIWindow(windowScene: windowScene)

    let vc1 = UIViewController()
    vc1.view.backgroundColor = .systemRed
    vc1.title = "Hello"
    let nc1 = UINavigationController(rootViewController: vc1)
    
    let vc2 = UIViewController()
    vc2.view.backgroundColor = .systemGreen
    vc2.title = "World"
    let nc2 = UINavigationController(rootViewController: vc2)

    let tb = UITabBarController()
    window?.rootViewController = tb
    window?.makeKeyAndVisible()
    
    tb.viewControllers = [nc1, nc2]
}

This is giving me layout constraint errors on iPadOS (not on iOS) as soon as I switch to the second "World" tab:

Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x600002139400 UILayoutGuide:0x600003b089a0'TitleView(layout=0x1035110f0)'.trailing <= UILayoutGuide:0x600003b08d20'UIViewLayoutMarginsGuide'.trailing   (active)>",
    "<NSLayoutConstraint:0x600002147480 H:|-(338)-[UILayoutGuide:0x600003b09180'TabBarGuide(0x103510de0)'](LTR)   (active, names: '|':_UINavigationBarContentView:0x103510de0 )>",
    "<NSLayoutConstraint:0x600002147610 UILayoutGuide:0x600003b09180'TabBarGuide(0x103510de0)'.width == 158   (active)>",
    "<NSLayoutConstraint:0x600002147980 UILayoutGuide:0x600003b089a0'TitleView(layout=0x1035110f0)'.trailing >= UILayoutGuide:0x600003b09180'TabBarGuide(0x103510de0)'.trailing   (active)>",
    "<NSLayoutConstraint:0x600002147b10 'UIView-Encapsulated-Layout-Width' _UINavigationBarContentView:0x103510de0.width == 0   (active)>",
    "<NSLayoutConstraint:0x600002138af0 'UIView-rightMargin-guide-constraint' H:[UILayoutGuide:0x600003b08d20'UIViewLayoutMarginsGuide']-(0)-|(LTR)   (active, names: '|':_UINavigationBarContentView:0x103510de0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600002139400 UILayoutGuide:0x600003b089a0'TitleView(layout=0x1035110f0)'.trailing <= UILayoutGuide:0x600003b08d20'UIViewLayoutMarginsGuide'.trailing   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

When I simply change from:

tb.viewControllers = [nc1, nc2]

to:

tb.viewControllers = [vc1, vc2]

the errors go away.

What is wrong here?


Solution

  • This is ultimately Apple's bug. You should report it and (for purposes of your app) ignore it.

    One way to prove this to yourself is that the same view hierarchy created entirely in the storyboard behaves the same way — you get the autolayout conflict on the first appearance of each tab.

    Another way is to reject Apple's new "floating tab bar" (the interface with the tab bar at the top), as described in How to disable the new UITabBarController view style in iPadOS 18. When you do, the issue goes away.

    So the problem is caused by some interaction of the new "floating tab bar" with the navigation bar, which sort of makes sense in that the "floating tab bar" covers the navigation bar; and you shouldn't worry about it. But, since the tab bar does cover the navigation bar, it might be best not use this architecture at all, i.e. don't put a navigation controller inside a tab bar controller in the first place.