From d85127cbe1ff97b77883d41e77ca9fed4bed6372 Mon Sep 17 00:00:00 2001 From: Guy Carmeli Date: Thu, 3 Sep 2026 10:54:57 +0300 Subject: [PATCH 1/5] fix(ios): handle bottom tab visibility on iOS 18 --- ios/BottomTabsBasePresenter.mm | 5 ++++ ios/RNNBottomTabsController.mm | 2 +- ios/RNNComponentViewController.mm | 3 ++- ios/UITabBarController+RNNOptions.h | 2 ++ ios/UITabBarController+RNNOptions.mm | 18 +++++++++++++ .../BottomTabsControllerTest.mm | 27 +++++++++++++++++++ .../RNNBottomTabsAppearancePresenterTest.mm | 12 +++++++++ .../UITabBarController+RNNOptionsTest.mm | 27 +++++++++++++++++++ playground/src/screens/LayoutsScreen.tsx | 19 +++++++++++++ playground/src/testIDs.ts | 1 + 10 files changed, 114 insertions(+), 2 deletions(-) diff --git a/ios/BottomTabsBasePresenter.mm b/ios/BottomTabsBasePresenter.mm index 044d766f473..97da214294f 100644 --- a/ios/BottomTabsBasePresenter.mm +++ b/ios/BottomTabsBasePresenter.mm @@ -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] + animated:NO]; + } [bottomTabs setCurrentTabIndex:[withDefault.bottomTabs.currentTabIndex withDefault:0]]; if (withDefault.bottomTabs.currentTabId.hasValue) { [bottomTabs setCurrentTabID:withDefault.bottomTabs.currentTabId.get]; diff --git a/ios/RNNBottomTabsController.mm b/ios/RNNBottomTabsController.mm index f48ffd17bc5..05792736d72 100644 --- a/ios/RNNBottomTabsController.mm +++ b/ios/RNNBottomTabsController.mm @@ -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]; } diff --git a/ios/RNNComponentViewController.mm b/ios/RNNComponentViewController.mm index 97cc785a1b9..6bef39cf199 100644 --- a/ios/RNNComponentViewController.mm +++ b/ios/RNNComponentViewController.mm @@ -1,5 +1,6 @@ #import "RNNComponentViewController.h" #import "AnimationObserver.h" +#import "UITabBarController+RNNOptions.h" @implementation RNNComponentViewController { NSArray *_reactViewConstraints; @@ -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; } diff --git a/ios/UITabBarController+RNNOptions.h b/ios/UITabBarController+RNNOptions.h index fb50bc194f9..545ba9c6da1 100644 --- a/ios/UITabBarController+RNNOptions.h +++ b/ios/UITabBarController+RNNOptions.h @@ -20,6 +20,8 @@ - (void)hideTabBar:(BOOL)animated; +- (BOOL)rnn_isTabBarHidden; + - (void)syncTabBarItemTestIDs; @end diff --git a/ios/UITabBarController+RNNOptions.mm b/ios/UITabBarController+RNNOptions.mm index 8d8c5004ada..465f7a04acb 100644 --- a/ios/UITabBarController+RNNOptions.mm +++ b/ios/UITabBarController+RNNOptions.mm @@ -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, @@ -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, @@ -182,6 +192,14 @@ - (void)hideTabBar:(BOOL)animated { } } +- (BOOL)rnn_isTabBarHidden { + if (@available(iOS 18.0, *)) { + return self.tabBarHidden; + } + + return self.tabBar.hidden; +} + - (void)forEachTab:(void (^)(UIView *, UIViewController *tabViewController, int tabIndex))performOnTab { int tabIndex = 0; diff --git a/playground/ios/NavigationTests/BottomTabsControllerTest.mm b/playground/ios/NavigationTests/BottomTabsControllerTest.mm index c43d7f36461..5edd517801a 100644 --- a/playground/ios/NavigationTests/BottomTabsControllerTest.mm +++ b/playground/ios/NavigationTests/BottomTabsControllerTest.mm @@ -161,6 +161,33 @@ - (void)testGetCurrentChild_shouldReturnSelectedViewController { [(RNNBottomTabsController *)self.uut selectedViewController]); } +- (void)testSetTabBarVisible_shouldUseActiveTabBarVisibilityOnIOS18 { + 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]; diff --git a/playground/ios/NavigationTests/RNNBottomTabsAppearancePresenterTest.mm b/playground/ios/NavigationTests/RNNBottomTabsAppearancePresenterTest.mm index d74df9fa587..2fa33be0e69 100644 --- a/playground/ios/NavigationTests/RNNBottomTabsAppearancePresenterTest.mm +++ b/playground/ios/NavigationTests/RNNBottomTabsAppearancePresenterTest.mm @@ -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"]; diff --git a/playground/ios/NavigationTests/UITabBarController+RNNOptionsTest.mm b/playground/ios/NavigationTests/UITabBarController+RNNOptionsTest.mm index 9d47153b4d7..35e8d971c22 100644 --- a/playground/ios/NavigationTests/UITabBarController+RNNOptionsTest.mm +++ b/playground/ios/NavigationTests/UITabBarController+RNNOptionsTest.mm @@ -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 diff --git a/playground/src/screens/LayoutsScreen.tsx b/playground/src/screens/LayoutsScreen.tsx index b78704b17c5..91b128e356f 100644 --- a/playground/src/screens/LayoutsScreen.tsx +++ b/playground/src/screens/LayoutsScreen.tsx @@ -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, @@ -78,6 +79,11 @@ export default class LayoutsScreen extends NavigationComponent