Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ios/BottomTabsBasePresenter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ - (void)applyOptionsOnInit:(RNNNavigationOptions *)options {
[super applyOptionsOnInit:options];
UITabBarController *bottomTabs = self.tabBarController;
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.

animated:NO];
}
[bottomTabs setCurrentTabIndex:[withDefault.bottomTabs.currentTabIndex withDefault:0]];
if (withDefault.bottomTabs.currentTabId.hasValue) {
[bottomTabs setCurrentTabID:withDefault.bottomTabs.currentTabId.get];
Expand Down
2 changes: 1 addition & 1 deletion ios/RNNBottomTabsController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ - (void)layoutCustomRow {
tabBarInView.size.width, desiredHeight);

_customRow.frame = rowFrame;
_customRow.hidden = self.tabBar.hidden;
_customRow.hidden = [self rnn_isTabBarHidden];
[_customRow setSelectedIndex:_currentTabIndex];
}

Expand Down
3 changes: 2 additions & 1 deletion ios/RNNComponentViewController.mm
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#import "RNNComponentViewController.h"
#import "AnimationObserver.h"
#import "UITabBarController+RNNOptions.h"

@implementation RNNComponentViewController {
NSArray *_reactViewConstraints;
Expand Down Expand Up @@ -126,7 +127,7 @@ - (void)updateReactViewFrame {
}

- (BOOL)shouldDrawBehindBottomTabs {
return !self.tabBarController.tabBar || self.tabBarController.tabBar.isHidden ||
return !self.tabBarController.tabBar || [self.tabBarController rnn_isTabBarHidden] ||
_drawBehindBottomTabs;
}

Expand Down
2 changes: 2 additions & 0 deletions ios/UITabBarController+RNNOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

- (void)hideTabBar:(BOOL)animated;

- (BOOL)rnn_isTabBarHidden;

- (void)syncTabBarItemTestIDs;

@end
18 changes: 18 additions & 0 deletions ios/UITabBarController+RNNOptions.mm
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ - (void)centerTabItems {
}

- (void)showTabBar:(BOOL)animated {
if (@available(iOS 18.0, *)) {
[self setTabBarHidden:NO animated:animated];
return;
}

static const CGFloat animationDuration = 0.15;
const CGRect tabBarVisibleFrame = CGRectMake(
self.tabBar.frame.origin.x, self.view.frame.size.height - self.tabBar.frame.size.height,
Expand All @@ -161,6 +166,11 @@ - (void)showTabBar:(BOOL)animated {
}

- (void)hideTabBar:(BOOL)animated {
if (@available(iOS 18.0, *)) {
[self setTabBarHidden:YES animated:animated];
return;
}

static const CGFloat animationDuration = 0.15;
const CGRect tabBarHiddenFrame =
CGRectMake(self.tabBar.frame.origin.x, self.view.frame.size.height,
Expand All @@ -182,6 +192,14 @@ - (void)hideTabBar:(BOOL)animated {
}
}

- (BOOL)rnn_isTabBarHidden {
if (@available(iOS 18.0, *)) {
return self.tabBarHidden;
}

return self.tabBar.hidden;
}
Comment thread
guyca marked this conversation as resolved.

- (void)forEachTab:(void (^)(UIView *, UIViewController *tabViewController,
int tabIndex))performOnTab {
int tabIndex = 0;
Expand Down
27 changes: 27 additions & 0 deletions playground/ios/NavigationTests/BottomTabsControllerTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,33 @@ - (void)testGetCurrentChild_shouldReturnSelectedViewController {
[(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.

if (@available(iOS 18.0, *)) {
[self.originalUut setTabBarVisible:NO];
XCTAssertTrue(self.originalUut.tabBarHidden);

[self.originalUut setTabBarVisible:YES];
XCTAssertFalse(self.originalUut.tabBarHidden);
}
}

- (void)testSetTabBarVisible_shouldNotOverrideStackChildVisibilityOnIOS18 {
if (@available(iOS 18.0, *)) {
UIViewController *component =
[RNNComponentViewController createWithComponentId:@"componentId"
initialOptions:[RNNNavigationOptions emptyOptions]];
UINavigationController *stack =
[[UINavigationController alloc] initWithRootViewController:component];
RNNBottomTabsController *uut =
[RNNBottomTabsController createWithChildren:@[ stack ]];

[uut setTabBarHidden:NO animated:NO];
[uut setTabBarVisible:NO];

XCTAssertFalse(uut.tabBarHidden);
}
}

- (void)testPreferredStatusBarStyle_shouldInvokeSelectedViewControllerPreferredStatusBarStyle {
[[self.mockTabBarPresenter expect] getStatusBarStyle];
[self.uut preferredStatusBarStyle];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ - (void)testApplyOptionsOnInit_alwaysShow_shouldNotCenterTabImages {
[self.boundViewController verify];
}

- (void)testApplyOptionsOnInit_shouldApplyInitialVisibilityOnIOS18 {
if (@available(iOS 18.0, *)) {
RNNNavigationOptions *initialOptions = [RNNNavigationOptions emptyOptions];
initialOptions.bottomTabs.visible = [[Bool alloc] initWithValue:@(0)];
[[self.boundViewController expect] setTabBarHidden:YES animated:NO];

[self.uut applyOptionsOnInit:initialOptions];

[self.boundViewController verify];
}
}

- (void)testApplyOptions_shouldApplyOptionsOnInit_alwaysHide_shouldCenterTabImages {
RNNNavigationOptions *initialOptions = [RNNNavigationOptions emptyOptions];
initialOptions.bottomTabs.titleDisplayMode = [[Text alloc] initWithValue:@"alwaysHide"];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,31 @@ - (void)test_tabBarHideShadow_false {
XCTAssertFalse(self.uut.tabBar.clipsToBounds);
}

- (void)test_hideTabBar {
if (@available(iOS 18.0, *)) {
[self.uut hideTabBar:NO];
XCTAssertTrue(self.uut.tabBarHidden);
XCTAssertTrue([self.uut rnn_isTabBarHidden]);
}
}

- (void)test_showTabBar {
if (@available(iOS 18.0, *)) {
self.uut.tabBarHidden = YES;
[self.uut showTabBar:NO];
XCTAssertFalse(self.uut.tabBarHidden);
XCTAssertFalse([self.uut rnn_isTabBarHidden]);
}
}

- (void)test_rnnIsTabBarHidden_shouldUseAvailableVisibilityState {
if (@available(iOS 18.0, *)) {
self.uut.tabBarHidden = YES;
} else {
self.uut.tabBar.hidden = YES;
}

XCTAssertTrue([self.uut rnn_isTabBarHidden]);
}

@end
19 changes: 19 additions & 0 deletions playground/src/screens/LayoutsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const {
WELCOME_SCREEN_HEADER,
STACK_BTN,
BOTTOM_TABS_BTN,
SINGLE_BOTTOM_TAB_MODAL_BTN,
BOTTOM_TABS,
SIDE_MENU_BTN,
KEYBOARD_SCREEN_BTN,
Expand Down Expand Up @@ -78,6 +79,11 @@ export default class LayoutsScreen extends NavigationComponent<NavigationProps,
<Root componentId={this.props.componentId}>
<Button label="Stack" testID={STACK_BTN} onPress={this.stack} />
<Button label="BottomTabs" testID={BOTTOM_TABS_BTN} onPress={this.bottomTabs} />
<Button
label="Single BottomTab Modal"
testID={SINGLE_BOTTOM_TAB_MODAL_BTN}
onPress={this.singleBottomTabModal}
/>
<Button label="BottomTabs Styling" onPress={this.bottomTabsStyling} />
<Button
label="BottomTabs with Role"
Expand Down Expand Up @@ -122,6 +128,19 @@ export default class LayoutsScreen extends NavigationComponent<NavigationProps,
});
};

singleBottomTabModal = () => {
Navigation.showModal({
bottomTabs: {
children: [stack(Screens.FirstBottomTabsScreen)],
options: {
bottomTabs: {
visible: false,
},
},
},
});
};

bottomTabsStyling = () => {
Navigation.showModal({
bottomTabs: {
Expand Down
1 change: 1 addition & 0 deletions playground/src/testIDs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const testIDs = {
MODAL_DISMISSED_LISTENER_RESULT: 'MODAL_DISMISSED_LISTENER_RESULT',
DISMISS_MODAL_PROMISE_RESULT: 'DISMISS_MODAL_PROMISE_RESULT',
BOTTOM_TABS_BTN: 'BOTTOM_TABS_BUTTON',
SINGLE_BOTTOM_TAB_MODAL_BTN: 'SINGLE_BOTTOM_TAB_MODAL_BUTTON',
EXTERNAL_COMP_BTN: 'EXTERNAL_COMPONENT_BUTTON',
LAYOUTS_TAB: 'LAYOUTS_TAB',
NAVIGATION_TAB: 'NAVIGATION_TAB',
Expand Down