Skip to content

iOS: Fix bottom tabs visibility on iOS 18 - #8363

Open
guyca wants to merge 5 commits into
masterfrom
fix/ios-18-bottom-tabs-visibility
Open

iOS: Fix bottom tabs visibility on iOS 18#8363
guyca wants to merge 5 commits into
masterfrom
fix/ios-18-bottom-tabs-visibility

Conversation

@guyca

@guyca guyca commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Restore bottom-tabs visibility across iOS versions after adopting the iOS 18 API. Resolved visibility now reconciles native state while retaining OS animation semantics.

  • reconcile resolved bottomTabs.visible on iOS 18+; preserve legacy stack guard
  • default bottomTabs.animate to true; honor explicit and global false
  • keep initial layout visibility non-animated
  • avoid UIKit mutations when visibility already matches
  • add presenter and controller regression coverage
  • validate 57 native tests and full local iOS Detox: 20 suites, 173 passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

tabBarHidden requires an SDK-version guard to prevent compilation failures with Xcode 15 or earlier.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates iOS bottom-tab visibility handling for iOS 18 while preserving legacy behavior.

Changes:

  • Uses version-aware tab-bar visibility APIs.
  • Adds native regression tests.
  • Adds a playground reproduction.
File summaries
File Description
playground/src/testIDs.ts Adds the reproduction test ID.
playground/src/screens/LayoutsScreen.tsx Adds a hidden bottom-tab modal reproduction.
playground/ios/NavigationTests/UITabBarController+RNNOptionsTest.mm Tests version-aware visibility helpers.
playground/ios/NavigationTests/RNNBottomTabsAppearancePresenterTest.mm Tests initial hidden-state handling.
playground/ios/NavigationTests/BottomTabsControllerTest.mm Tests controller visibility behavior and stack precedence.
ios/UITabBarController+RNNOptions.mm Adds iOS 18 visibility handling, but lacks an SDK preprocessor guard for older Xcode toolchains.
ios/UITabBarController+RNNOptions.h Declares the visibility-state helper.
ios/RNNComponentViewController.mm Uses active tab-bar visibility for layout.
ios/RNNBottomTabsController.mm Synchronizes custom-row visibility.
ios/BottomTabsBasePresenter.mm Applies initial iOS 18 visibility.
Review details
  • Files reviewed: 10/10 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ios/UITabBarController+RNNOptions.mm
@guyca

guyca commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

#rebuild

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Restore the unrelated RN 0.77 CI jobs or move and document that support-policy change separately.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread .buildkite/pipeline.sh

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The stale iOS 12 tests, animation regressions, and unrelated CI coverage removals must be addressed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

.buildkite/jobs/pipeline.android_rn_77.yml:1

  • Removing the Android RN 0.77 job (together with its iOS counterpart and pipeline entries) silently drops compatibility coverage, but this PR is described solely as an iOS bottom-tabs fix. Restore these jobs or move/document the RN 0.77 support removal as a separate intentional change.

.buildkite/pipeline.sh:6

  • This also drops both RN 0.77 Android and iOS jobs from the CI matrix, which is unrelated to the described iOS 18 tab-bar fix and silently reduces compatibility coverage. Restore these entries, or explicitly document and separate the intentional RN 0.77 support change so it can be reviewed independently.
cat .buildkite/jobs/pipeline.release.yml
cat .buildkite/jobs/pipeline.android_rn_78.yml

ios/BottomTabsBasePresenter.mm:33

  • This selector change leaves the active playgroundIOS12 scheme's RNNBottomTabsPresenterTest expectations at setTabBarVisible: (lines 31 and 58). Both tests now fail OCMock verification because production calls reconcileTabBarVisible:animated: instead; update that legacy test target alongside the current tests.
    [bottomTabs reconcileTabBarVisible:[withDefault.bottomTabs.visible withDefault:YES]
                              animated:[withDefault.bottomTabs.animate withDefault:YES]];
  • Files reviewed: 14/14 changed files
  • Comments generated: 3
  • Review effort level: Balanced

Comment thread ios/BottomTabsBasePresenter.mm Outdated
Comment thread ios/BottomTabsBasePresenter.mm Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Legacy iOS test expectations are broken, and supported RN 0.77 CI coverage is removed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

.buildkite/pipeline.sh:9

  • This removes both iOS and Android CI coverage for React Native 0.77 even though ARCHITECTURE.md:9 still defines the supported range as React Native 0.77+, and the PR only describes an iOS bottom-tabs fix. Keep the minimum-version jobs, or make the support-policy change explicit and update the compatibility documentation in a separate scoped change.
cat .buildkite/jobs/pipeline.release.yml
cat .buildkite/jobs/pipeline.android_rn_78.yml
cat .buildkite/jobs/pipeline.android_rn_84.yml
cat .buildkite/jobs/pipeline.android_rn_85.yml
cat .buildkite/jobs/pipeline.ios_rn_78.yml
  • Files reviewed: 14/14 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread ios/BottomTabsBasePresenter.mm

@yedidyak yedidyak left a comment

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.

Reviewed the iOS 18 visibility reconciliation, the presenter animation defaults, and the added native coverage. The reconcileTabBarVisible:animated: state comparison and the rnn_isTabBarHidden read-side unification look correct, and the pre-18 path is preserved. Left inline comments on test coverage, the animation default, and the initial-visibility path. One more thing:

viewWillAppear hack on iOS 18. RNNBottomTabsController.mm:93-101 still pushes and pops a throwaway view controller to force an initially-hidden tab bar. Now that applyOptionsOnInit sets tabBarHidden directly on 18+, is that hack still needed there? The dummy controller has hidesBottomBarWhenPushed == NO, so it may briefly work against the new API. Gating it to pre-18 would keep the two mechanisms from overlapping.

Nit: playground/ios/NavigationIOS12Tests/RNNBottomTabsPresenterTest.mm:31 and :58 still expect setTabBarVisible:, which production no longer calls. That target only builds under the playgroundIOS12 scheme and no script invokes it, so nothing fails today — worth updating or deleting the target while you're here.

[(RNNBottomTabsController *)self.uut selectedViewController]);
}

- (void)testSetTabBarVisible_shouldUseActiveTabBarVisibilityOnIOS18 {

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.

None of the coverage added for this fix actually runs on an iOS 18+ runtime in CI, from either direction:

  • The iOS unit tests run against iOS 15.5 — scripts/test-unit.js:29 is testTarget('playground', 'iPhone 13', '15.5'). Every case added here is wrapped in if (@available(iOS 18.0, *)), so those bodies are skipped and the tests pass without asserting anything. Same for the new cases in RNNBottomTabsAppearancePresenterTest and UITabBarController+RNNOptionsTest. Only the BeforeIOS18 branches execute.
  • The e2e job does run where the new code path is live (playground/e2e/detox.config.js:24, iPhone 17 Pro Max / 26.1), but the playground reproduction added in this PR has no Detox spec — SINGLE_BOTTOM_TAB_MODAL_BTN is wired to a button and never referenced by a test.

So the suite that runs on a modern OS doesn't exercise this, and the tests that exercise it don't run on a modern OS. An e2e case on the new repro screen asserting the tab bar stays hidden looks like the cheapest fix, since that job is already on iOS 26. Bumping the unit-test destination to iOS 18+ would work too, though it changes what the whole suite verifies.

[bottomTabs setTabBarVisible:mergeOptions.bottomTabs.visible.get animated:NO];
}
[bottomTabs setTabBarVisible:mergeOptions.bottomTabs.visible.get
animated:[withDefault.bottomTabs.animate withDefault:YES]];

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.

This flips the effective default for bottomTabs.animate from off to on, and not just on iOS 18. Previously applyOptions always applied visibility non-animated via setTabBarVisible:, and mergeOptions animated only when animate was explicitly provided. With withDefault:YES here and at line 48, pushing a screen with bottomTabs.visible: false now animates the tab bar on iOS 13–17 too.

Aligning with Android (BottomTabsPresenter.kt uses animate.isTrueOrUndefined) seems right, but the new default is load-bearing now and isn't documented — OptionsBottomTabs.animate in src/interfaces/Options.ts carries no @default. Worth adding @default true there and calling the change out, since it's visible on every iOS version rather than only the one in the PR title.

RNNNavigationOptions *withDefault = [options withDefault:[self defaultOptions]];
if (@available(iOS 18.0, *)) {
[bottomTabs
setTabBarHidden:![withDefault.bottomTabs.visible withDefault:YES]

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.

This calls UIKit's setTabBarHidden:animated: directly rather than going through showTabBar:/hideTabBar: like every other visibility path in the controller, so the initial application skips _tabBarNeedsRestore bookkeeping and the custom-row sync. Routing it through the category helpers would keep a single code path for visibility changes.

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