Skip to content

fix(iOS, Tabs): fix moreNavigationController navigation bar visible on nav#3993

Merged
kkafar merged 9 commits intomainfrom
@kkafar/more-navigation-controller-header
May 8, 2026
Merged

fix(iOS, Tabs): fix moreNavigationController navigation bar visible on nav#3993
kkafar merged 9 commits intomainfrom
@kkafar/more-navigation-controller-header

Conversation

@kkafar
Copy link
Copy Markdown
Member

@kkafar kkafar commented May 7, 2026

Description

Fixes an issue on iOS Tabs where the moreNavigationController's navigation bar would be briefly visible and then animated away after the user navigated for the first time to a screen hosted inside the more navigation controller.

Also patches a closely related issue (not covered by the linked issue) where the navigation bar would briefly appear and animate away during transitions to/from the more navigation controller triggered by a horizontal size class change.

Closes: https://github.com/software-mansion/react-native-screens-labs/issues/1101

Changes

  • RNSTabBarController.mm:
    • In updateSelectedViewControllerInner, preemptively hide the navigation bar when the nextSelectedViewController is hosted inside the more navigation controller.
    • Tightened canHaveMoreNavigationController to also require horizontalSizeClass == UIUserInterfaceSizeClassCompact, so the check correctly reflects when the more nav. controller is actually present.
    • Added isViewControllerPresentedFromTheMoreNavigationController: helper that combines the above with the tab bar items check and the index check (accounting for the slot replaced by the more nav. controller).
    • Overrode traitCollectionDidChange: to handle size-class transitions (call super first, then query self.selectedViewController so UIKit has updated its model). Used the deprecated API instead of registerForTraitCollectionChange:withAction: because the latter is iOS 17+ and we still support iOS 15.
    • Added an RCTAssert that _pendingStateUpdate must not be nil in updateSelectedViewControllerInner.
    • Extracted magic number 5 into kMinCountOfVCsForMoreVCPresence constant with a link to the Apple docs.

Visual documentation

before after
1101-before.mov
1101-after.mov

Test plan

Manual repro on iOS (iPhone, compact horizontal size class):

  1. Run FabricExample on iOS.
  2. Open the Tabs example with more than 5 tab screens so the More tab appears.
  3. Tap More, then select a screen hosted inside the more navigation controller.
  4. Before the fix: the navigation bar of the more navigation controller is visible for a moment, then animates away.
  5. After the fix: navigation bar is hidden preemptively, no flicker.

Additional case (size-class transitions):

  1. Same setup as above on a device/simulator that supports size-class changes (e.g. iPad split view, or device rotation where applicable).
  2. Trigger a horizontal size-class change while a more-nav-hosted screen is selected.
  3. Before the fix: nav bar flashes and animates away.
  4. After the fix: nav bar stays hidden across the transition.

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

after first nav.

We had an ugly error, where the navigation bar of more navigation
controller would be visible & animated away from the user interface
after the user had navigated for the first time to a screen that's
hosted inside the more navigation controller.

To mitigate that I've updated the `updateSelectedViewControllerInner` method.
It now checks, whether the `nextSelectedViewController` is hosted inside
the *more navigation controller* and if so, it preemptively hides the
navigation bar.

To correctly detect "whether the `nextSelectedViewController` is hosted
inside the *more navigation controller*" I've tightened the
`canHaveMoreNavigationController` condition, by verifying whether we're
in appropriate size class at the moment of check.

It has not been described by linked issue software-mansion/react-native-screens-labs#1101,
but we had very similar issue, where the navigation bar would animate away after
being visible for a moment on transition to and from more navigation controller,
that happens when the size class changes. I've patched it by overriding the `traitCollectionDidChange:`
method and hiding the navigation bar from there at appropriate moment. It's
important to note, that we perform our operation AFTER calling super. We give
UIKit time to update the model, before we query `self.selectedViewController`.

I'm not using `registerForTraitCollectionChange:withAction:` API in lieu
of deprecated `traitCollectionDidChange:`, because it's available only
since iOS 17, while we need to keep compat with iOS 15.
@kkafar
Copy link
Copy Markdown
Member Author

kkafar commented May 7, 2026

It's open question, whether in cases like that should we extend the scenario for the test-tabs-more-navigation-controller, create a dedicated issue test or even do nothing (as right now.

cc @LKuchno

@kkafar kkafar marked this pull request as ready for review May 7, 2026 18:13
@kkafar kkafar requested review from Copilot, kligarski, kmichalikk, sgaczol and t0maboro and removed request for Copilot and t0maboro May 7, 2026 18:13
@kkafar kkafar changed the title fix(iOS, Tabs): hide nav bar in moreNavigationController on first nav fix(iOS, Tabs): fix moreNavigationController navigation bar visible on nav May 7, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses an iOS Tabs UI glitch where moreNavigationController’s navigation bar briefly appears (then hides) when navigating to tabs hosted under the “More” controller, including during horizontal size-class transitions.

Changes:

  • Adds logic to proactively hide moreNavigationController’s navigation bar when selecting a screen hosted under “More”.
  • Tightens the “More is available” check to require compact horizontal size class and introduces a helper to detect “More-hosted” view controllers.
  • Hooks into traitCollectionDidChange: to handle size-class transitions affecting the More controller.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ios/tabs/host/RNSTabBarController.mm Outdated
Comment thread ios/tabs/host/RNSTabBarController.mm Outdated
Comment thread ios/tabs/host/RNSTabBarController.mm Outdated
kkafar added 4 commits May 7, 2026 20:39
5 is the index. 6 is the count of required controllers.
`isViewControllerPresentedFromTheMoreNavigationController`
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.

Comment thread ios/tabs/host/RNSTabBarController.mm
Address review feedback on the moreNavigationController fix:

- Rename isViewControllerPresentedFromTheMoreNavigationController to
  isViewControllerHostedByMoreNavigationController. "Presented" in UIKit
  implies modal presentation; "hosted by" reflects the actual relationship.
- Guard against NSNotFound before comparing the index. indexOfObject:
  returns NSUIntegerMax when the view controller is absent from
  self.viewControllers (e.g. moreNavigationController itself), which
  previously passed the >= check and could yield a false positive.
- Extract kFirstIndexInMoreNavigationController = 4 instead of deriving
  it as kMinCountOfVCsForMoreVCPresence - 2. The two are independent
  UIKit facts (threshold for More to appear vs first index hosted in
  More); the old formula coupled them by coincidence.
- Note in canHaveMoreNavigationController that the size class check is
  empirical, so future readers know it can be revisited.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.

Comment thread ios/tabs/host/RNSTabBarController.mm Outdated
Comment thread ios/tabs/host/RNSTabBarController.mm Outdated
kkafar and others added 2 commits May 7, 2026 21:31
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The index-based check in isViewControllerHostedByMoreNavigationController:
was unsound under tab reordering. UITabBarController lets users reorder
tabs through the More list's Edit button; after a reorder, a VC originally
placed at index < 4 in self.viewControllers may end up hosted by More
(and vice versa). The index in self.viewControllers does not move when
this happens, so the check returned the wrong answer for reordered
configurations and would have reintroduced the navigation-bar flicker
this PR is meant to fix.

Replace the index comparison with a direct ground-truth check: if the
VC's tabBarItem is not currently in self.tabBar.items, it is hosted by
the More navigation controller. The NSNotFound guard is kept to
explicitly reject arbitrary external VCs. The now-unused
kFirstIndexInMoreNavigationController constant is removed.
@kkafar kkafar requested a review from Copilot May 7, 2026 19:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

{
[super traitCollectionDidChange:previousTraitCollection];

if (previousTraitCollection == nil || self.selectedViewController == nil) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: you may consider combining all 4 conditions, I don't see any reason for splitting this, until you're planning to add some logic/logs before that early return disableNavigationBarInMoreNavigationController

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that, I think its just thing of aesthetics - I did that deliberately to separate the guard statements from actual logic. I'll leave it as is.

@kkafar kkafar merged commit f489142 into main May 8, 2026
10 checks passed
@kkafar kkafar deleted the @kkafar/more-navigation-controller-header branch May 8, 2026 11:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants