-
Notifications
You must be signed in to change notification settings - Fork 2.6k
iOS: Fix bottom tabs visibility on iOS 18 #8363
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d85127c
7e55ccc
c191ae4
ebf7bfd
cbb900d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,31 @@ | |
| #import "RNNConvert.h" | ||
| #import "UIImage+utils.h" | ||
|
|
||
| @implementation BottomTabsBasePresenter | ||
| @implementation BottomTabsBasePresenter { | ||
| BOOL _didApplyInitialTabBarVisibility; | ||
| } | ||
|
|
||
| - (BOOL)tabBarVisibilityAnimation:(BOOL)animated { | ||
| if (@available(iOS 18.0, *)) { | ||
| return animated; | ||
| } | ||
| if (_didApplyInitialTabBarVisibility) { | ||
|
guyca marked this conversation as resolved.
|
||
| return animated; | ||
| } | ||
|
|
||
| _didApplyInitialTabBarVisibility = YES; | ||
| return NO; | ||
| } | ||
|
|
||
| - (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] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This calls UIKit's |
||
| animated:NO]; | ||
| } | ||
| [bottomTabs setCurrentTabIndex:[withDefault.bottomTabs.currentTabIndex withDefault:0]]; | ||
| if (withDefault.bottomTabs.currentTabId.hasValue) { | ||
| [bottomTabs setCurrentTabID:withDefault.bottomTabs.currentTabId.get]; | ||
|
|
@@ -24,7 +43,9 @@ - (void)applyOptions:(RNNNavigationOptions *)options { | |
| RNNNavigationOptions *withDefault = [options withDefault:[self defaultOptions]]; | ||
|
|
||
| [bottomTabs setTabBarTestID:[withDefault.bottomTabs.testID withDefault:nil]]; | ||
| [bottomTabs setTabBarVisible:[withDefault.bottomTabs.visible withDefault:YES]]; | ||
| [bottomTabs reconcileTabBarVisible:[withDefault.bottomTabs.visible withDefault:YES] | ||
| animated:[self tabBarVisibilityAnimation: | ||
| [withDefault.bottomTabs.animate withDefault:YES]]]; | ||
|
|
||
| [bottomTabs.view setBackgroundColor:[withDefault.layout.backgroundColor withDefault:nil]]; | ||
| [bottomTabs setTabBarHideShadow:[withDefault.bottomTabs.hideShadow withDefault:NO]]; | ||
|
|
@@ -74,12 +95,8 @@ - (void)mergeOptions:(RNNNavigationOptions *)mergeOptions | |
| } | ||
|
|
||
| if (mergeOptions.bottomTabs.visible.hasValue) { | ||
| if (mergeOptions.bottomTabs.animate.hasValue) { | ||
| [bottomTabs setTabBarVisible:mergeOptions.bottomTabs.visible.get | ||
| animated:[mergeOptions.bottomTabs.animate withDefault:NO]]; | ||
| } else { | ||
| [bottomTabs setTabBarVisible:mergeOptions.bottomTabs.visible.get animated:NO]; | ||
| } | ||
| [bottomTabs setTabBarVisible:mergeOptions.bottomTabs.visible.get | ||
| animated:[withDefault.bottomTabs.animate withDefault:YES]]; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This flips the effective default for Aligning with Android ( |
||
| } | ||
|
|
||
| if (mergeOptions.layout.backgroundColor.hasValue) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ | |
|
|
||
| - (void)hideTabBar:(BOOL)animated; | ||
|
|
||
| - (BOOL)rnn_isTabBarHidden; | ||
|
|
||
| - (void)syncTabBarItemTestIDs; | ||
|
|
||
| @end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -161,6 +161,127 @@ - (void)testGetCurrentChild_shouldReturnSelectedViewController { | |
| [(RNNBottomTabsController *)self.uut selectedViewController]); | ||
| } | ||
|
|
||
| - (void)testSetTabBarVisible_shouldUseActiveTabBarVisibilityOnIOS18 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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_shouldShowHiddenTabBarForStackChildOnIOS18 { | ||
| 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:YES animated:NO]; | ||
| [uut setTabBarVisible:YES]; | ||
| XCTAssertFalse(uut.tabBarHidden); | ||
| } | ||
| } | ||
|
|
||
| - (void)testSetTabBarVisible_shouldHideVisibleTabBarForStackChildOnIOS18 { | ||
| 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]; | ||
| XCTAssertTrue(uut.tabBarHidden); | ||
| } | ||
| } | ||
|
|
||
| - (void)testSetTabBarVisible_shouldNotUpdateMatchingVisibilityOnIOS18 { | ||
| if (@available(iOS 18.0, *)) { | ||
| [self.originalUut setTabBarHidden:YES animated:NO]; | ||
| id uutMock = self.uut; | ||
| [[uutMock reject] setTabBarVisible:NO animated:NO]; | ||
| [[uutMock reject] setTabBarVisible:NO animated:YES]; | ||
|
|
||
| [uutMock reconcileTabBarVisible:NO animated:YES]; | ||
|
|
||
| [uutMock verify]; | ||
| XCTAssertTrue(self.originalUut.tabBarHidden); | ||
| } | ||
| } | ||
|
|
||
| - (void)testReconcileTabBarVisible_shouldUseRequestedAnimationOnIOS18 { | ||
| if (@available(iOS 18.0, *)) { | ||
| [self.originalUut setTabBarHidden:YES animated:NO]; | ||
| id uutMock = self.uut; | ||
| [[uutMock expect] setTabBarVisible:YES animated:YES]; | ||
|
|
||
| [uutMock reconcileTabBarVisible:YES animated:YES]; | ||
|
|
||
| [uutMock verify]; | ||
| } | ||
| } | ||
|
|
||
| - (void)testReconcileTabBarVisible_shouldUseRequestedHideAnimationOnIOS18 { | ||
| if (@available(iOS 18.0, *)) { | ||
| [self.originalUut setTabBarHidden:NO animated:NO]; | ||
| id uutMock = self.uut; | ||
| [[uutMock expect] setTabBarVisible:NO animated:YES]; | ||
|
|
||
| [uutMock reconcileTabBarVisible:NO animated:YES]; | ||
|
|
||
| [uutMock verify]; | ||
| } | ||
| } | ||
|
|
||
| - (void)testReconcileTabBarVisible_shouldClearRestoreBookkeepingOnIOS18 { | ||
| if (@available(iOS 18.0, *)) { | ||
| [self.originalUut setTabBarVisible:NO animated:NO]; | ||
| XCTAssertTrue([[self.originalUut valueForKey:@"tabBarNeedsRestore"] boolValue]); | ||
|
|
||
| [self.originalUut reconcileTabBarVisible:NO animated:NO]; | ||
|
|
||
| XCTAssertFalse([[self.originalUut valueForKey:@"tabBarNeedsRestore"] boolValue]); | ||
| } | ||
| } | ||
|
|
||
| - (void)testReconcileTabBarVisible_shouldPreserveStackVisibilityBeforeIOS18 { | ||
| if (@available(iOS 18.0, *)) { | ||
| return; | ||
| } | ||
|
|
||
| UIViewController *component = | ||
| [RNNComponentViewController createWithComponentId:@"componentId" | ||
| initialOptions:[RNNNavigationOptions emptyOptions]]; | ||
| UINavigationController *stack = | ||
| [[UINavigationController alloc] initWithRootViewController:component]; | ||
| RNNBottomTabsController *uut = [RNNBottomTabsController createWithChildren:@[ stack ]]; | ||
|
|
||
| [uut reconcileTabBarVisible:NO animated:YES]; | ||
|
|
||
| XCTAssertFalse(uut.tabBar.hidden); | ||
| } | ||
|
|
||
| - (void)testReconcileTabBarVisible_shouldUseRequestedAnimationWithoutStackBeforeIOS18 { | ||
| if (@available(iOS 18.0, *)) { | ||
| return; | ||
| } | ||
|
|
||
| id uutMock = self.uut; | ||
| [[uutMock expect] setTabBarVisible:NO animated:YES]; | ||
|
|
||
| [uutMock reconcileTabBarVisible:NO animated:YES]; | ||
|
|
||
| [uutMock verify]; | ||
| } | ||
|
|
||
| - (void)testPreferredStatusBarStyle_shouldInvokeSelectedViewControllerPreferredStatusBarStyle { | ||
| [[self.mockTabBarPresenter expect] getStatusBarStyle]; | ||
| [self.uut preferredStatusBarStyle]; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.