iOS: Fix bottom tabs visibility on iOS 18 - #8363
Conversation
There was a problem hiding this comment.
🟡 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.
|
#rebuild |
There was a problem hiding this comment.
🟡 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
There was a problem hiding this comment.
🟡 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
playgroundIOS12scheme'sRNNBottomTabsPresenterTestexpectations atsetTabBarVisible:(lines 31 and 58). Both tests now fail OCMock verification because production callsreconcileTabBarVisible: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
There was a problem hiding this comment.
🟡 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:9still 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
yedidyak
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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:29istestTarget('playground', 'iPhone 13', '15.5'). Every case added here is wrapped inif (@available(iOS 18.0, *)), so those bodies are skipped and the tests pass without asserting anything. Same for the new cases inRNNBottomTabsAppearancePresenterTestandUITabBarController+RNNOptionsTest. Only theBeforeIOS18branches 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_BTNis 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]]; |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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.
Restore bottom-tabs visibility across iOS versions after adopting the iOS 18 API. Resolved visibility now reconciles native state while retaining OS animation semantics.
bottomTabs.visibleon iOS 18+; preserve legacy stack guardbottomTabs.animateto true; honor explicit and global false