diff --git a/.buildkite/jobs/pipeline.android_rn_77.yml b/.buildkite/jobs/pipeline.android_rn_77.yml deleted file mode 100644 index 565ad742ec..0000000000 --- a/.buildkite/jobs/pipeline.android_rn_77.yml +++ /dev/null @@ -1,16 +0,0 @@ - - label: ":android: Android (RN 0.77.3)" - env: - JAVA_HOME: /opt/openjdk/jdk-17.0.9.jdk/Contents/Home/ - REACT_NATIVE_VERSION: 0.77.3 - command: - - "nvm install" - - "./scripts/ci.android.sh" - key: "android_rn_77" - timeout_in_minutes: 60 - artifact_paths: "/Users/builder/uibuilder/work/playground/artifacts/**/*" - retry: - automatic: - - exit_status: [1, -1] - limit: 2 - - diff --git a/.buildkite/jobs/pipeline.ios_rn_77.yml b/.buildkite/jobs/pipeline.ios_rn_77.yml deleted file mode 100644 index 5b977f6b6d..0000000000 --- a/.buildkite/jobs/pipeline.ios_rn_77.yml +++ /dev/null @@ -1,15 +0,0 @@ - - label: ":ios: iOS (RN 0.77.3)" - env: - REACT_NATIVE_VERSION: 0.77.3 - command: - - "nvm install" - - "./scripts/ci.ios.sh" - key: "ios_rn_77" - timeout_in_minutes: 60 - artifact_paths: "/Users/builder/uibuilder/work/playground/artifacts/**/*" - retry: - automatic: - - exit_status: [1, -1] - limit: 2 - - diff --git a/.buildkite/pipeline.sh b/.buildkite/pipeline.sh index b9b14330b9..fb5185575f 100755 --- a/.buildkite/pipeline.sh +++ b/.buildkite/pipeline.sh @@ -3,11 +3,9 @@ echo "steps:" cat .buildkite/jobs/pipeline.release.yml -cat .buildkite/jobs/pipeline.android_rn_77.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_77.yml cat .buildkite/jobs/pipeline.ios_rn_78.yml cat .buildkite/jobs/pipeline.ios_rn_84.yml cat .buildkite/jobs/pipeline.ios_rn_85.yml diff --git a/ios/BottomTabsBasePresenter.mm b/ios/BottomTabsBasePresenter.mm index 044d766f47..caddeff053 100644 --- a/ios/BottomTabsBasePresenter.mm +++ b/ios/BottomTabsBasePresenter.mm @@ -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) { + 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] + 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]]; } if (mergeOptions.layout.backgroundColor.hasValue) { diff --git a/ios/RNNBottomTabsController.h b/ios/RNNBottomTabsController.h index 6efe764b26..b2ac14fd50 100644 --- a/ios/RNNBottomTabsController.h +++ b/ios/RNNBottomTabsController.h @@ -28,6 +28,8 @@ - (void)setTabBarVisible:(BOOL)visible; +- (void)reconcileTabBarVisible:(BOOL)visible animated:(BOOL)animated; + - (void)handleTabBarLongPress:(CGPoint)locationInTabBar; @end diff --git a/ios/RNNBottomTabsController.mm b/ios/RNNBottomTabsController.mm index f48ffd17bc..3d003af2b6 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]; } @@ -414,8 +414,21 @@ - (void)setTabBarVisible:(BOOL)visible animated:(BOOL)animated { } - (void)setTabBarVisible:(BOOL)visible { + [self reconcileTabBarVisible:visible animated:NO]; +} + +- (void)reconcileTabBarVisible:(BOOL)visible animated:(BOOL)animated { + if (@available(iOS 18.0, *)) { + BOOL shouldHide = !visible; + if (self.tabBarHidden != shouldHide) { + [self setTabBarVisible:visible animated:animated]; + } + _tabBarNeedsRestore = NO; + return; + } + if (_tabBarNeedsRestore || !self.presentedComponentViewController.navigationController) { - [self setTabBarVisible:visible animated:NO]; + [self setTabBarVisible:visible animated:animated]; _tabBarNeedsRestore = NO; } } diff --git a/ios/RNNComponentViewController.mm b/ios/RNNComponentViewController.mm index 97cc785a1b..6bef39cf19 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 fb50bc194f..545ba9c6da 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 8d8c5004ad..465f7a04ac 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 c43d7f3646..60c41215bf 100644 --- a/playground/ios/NavigationTests/BottomTabsControllerTest.mm +++ b/playground/ios/NavigationTests/BottomTabsControllerTest.mm @@ -161,6 +161,127 @@ - (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_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]; diff --git a/playground/ios/NavigationTests/RNNBottomTabsAppearancePresenterTest.mm b/playground/ios/NavigationTests/RNNBottomTabsAppearancePresenterTest.mm index d74df9fa58..e12d4332be 100644 --- a/playground/ios/NavigationTests/RNNBottomTabsAppearancePresenterTest.mm +++ b/playground/ios/NavigationTests/RNNBottomTabsAppearancePresenterTest.mm @@ -57,7 +57,7 @@ - (void)testApplyOptions_shouldSetDefaultEmptyOptions { [[self.boundViewController expect] setTabBarTestID:nil]; [[(id)self.uut expect] applyBackgroundColor:nil translucent:NO]; [[self.boundViewController expect] setTabBarHideShadow:NO]; - [[self.boundViewController expect] setTabBarVisible:YES]; + [[self.boundViewController expect] reconcileTabBarVisible:YES animated:YES]; [self.uut applyOptions:emptyOptions]; [self.boundViewController verify]; } @@ -75,6 +75,7 @@ - (void)testApplyOptions_shouldApplyOptions { [[(id)self.uut expect] applyBackgroundColor:nil translucent:[UIColor redColor]]; [[self.boundViewController expect] setTabBarHideShadow:YES]; [[self.boundViewController expect] setTabBarStyle:UIBarStyleBlack]; + [[self.boundViewController expect] reconcileTabBarVisible:NO animated:YES]; [self.uut applyOptions:initialOptions]; [self.boundViewController verify]; @@ -84,12 +85,98 @@ - (void)testApplyOptions_shouldRestoreHiddenTabBar { RNNNavigationOptions *initialOptions = [RNNNavigationOptions emptyOptions]; initialOptions.bottomTabs.visible = [[Bool alloc] initWithValue:@(1)]; - [[self.boundViewController expect] setTabBarVisible:YES]; + [[self.boundViewController expect] reconcileTabBarVisible:YES animated:YES]; [self.uut applyOptions:initialOptions]; [self.boundViewController verify]; } +- (void)testApplyOptions_shouldUseExplicitAnimation { + RNNNavigationOptions *options = [RNNNavigationOptions emptyOptions]; + options.bottomTabs.visible = [Bool withValue:NO]; + options.bottomTabs.animate = [Bool withValue:YES]; + [[self.boundViewController expect] reconcileTabBarVisible:NO animated:YES]; + + [self.uut applyOptions:options]; + + [self.boundViewController verify]; +} + +- (void)testApplyOptions_shouldAnimateVisibilityAfterInitialApplicationBeforeIOS18 { + if (@available(iOS 18.0, *)) { + return; + } + + [self.uut setValue:@NO forKey:@"didApplyInitialTabBarVisibility"]; + RNNNavigationOptions *options = [RNNNavigationOptions emptyOptions]; + options.bottomTabs.visible = [Bool withValue:NO]; + [[self.boundViewController expect] reconcileTabBarVisible:NO animated:NO]; + [self.uut applyOptions:options]; + [self.boundViewController verify]; + + [[self.boundViewController expect] reconcileTabBarVisible:NO animated:YES]; + [self.uut applyOptions:options]; + [self.boundViewController verify]; +} + +- (void)testApplyOptions_shouldDisableExplicitAnimation { + RNNNavigationOptions *options = [RNNNavigationOptions emptyOptions]; + options.bottomTabs.visible = [Bool withValue:NO]; + options.bottomTabs.animate = [Bool withValue:NO]; + [[self.boundViewController expect] reconcileTabBarVisible:NO animated:NO]; + + [self.uut applyOptions:options]; + + [self.boundViewController verify]; +} + +- (void)testApplyOptions_shouldUseConfiguredDefaultAnimation { + RNNNavigationOptions *defaultOptions = [RNNNavigationOptions emptyOptions]; + defaultOptions.bottomTabs.animate = [Bool withValue:NO]; + self.uut.defaultOptions = defaultOptions; + RNNNavigationOptions *options = [RNNNavigationOptions emptyOptions]; + options.bottomTabs.visible = [Bool withValue:NO]; + [[self.boundViewController expect] reconcileTabBarVisible:NO animated:NO]; + + [self.uut applyOptions:options]; + + [self.boundViewController verify]; +} + +- (void)testMergeOptions_shouldAnimateVisibilityByDefault { + RNNNavigationOptions *options = [RNNNavigationOptions emptyOptions]; + options.bottomTabs.visible = [Bool withValue:NO]; + [[self.boundViewController expect] setTabBarVisible:NO animated:YES]; + + [self.uut mergeOptions:options resolvedOptions:nil]; + + [self.boundViewController verify]; +} + +- (void)testMergeOptions_shouldDisableExplicitAnimation { + RNNNavigationOptions *options = [RNNNavigationOptions emptyOptions]; + options.bottomTabs.visible = [Bool withValue:NO]; + options.bottomTabs.animate = [Bool withValue:NO]; + [[self.boundViewController expect] setTabBarVisible:NO animated:NO]; + + [self.uut mergeOptions:options resolvedOptions:nil]; + + [self.boundViewController verify]; +} + +- (void)testMergeOptions_shouldUseConfiguredDefaultAnimation { + RNNNavigationOptions *defaultOptions = [RNNNavigationOptions emptyOptions]; + defaultOptions.bottomTabs.animate = [Bool withValue:NO]; + self.uut.defaultOptions = defaultOptions; + RNNNavigationOptions *options = [RNNNavigationOptions emptyOptions]; + options.bottomTabs.visible = [Bool withValue:NO]; + [[self.boundViewController expect] setTabBarVisible:NO animated:NO]; + + [self.uut mergeOptions:options resolvedOptions:nil]; + + [self.boundViewController verify]; +} + - (void)testApplyOptionsOnInit_alwaysShow_shouldNotCenterTabImages { RNNNavigationOptions *initialOptions = [RNNNavigationOptions emptyOptions]; initialOptions.bottomTabs.titleDisplayMode = [[Text alloc] initWithValue:@"alwaysShow"]; @@ -98,6 +185,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 9d47153b4d..35e8d971c2 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 b78704b17c..91b128e356 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