From dbb7430872d45f5a4ab8a8220d07739c00c4a285 Mon Sep 17 00:00:00 2001 From: Tymoteusz Boba Date: Tue, 17 Oct 2023 17:48:52 +0200 Subject: [PATCH] fix(iOS): change implementation of calculating status bar, refactor methods used on header height change (#1917) ## Description It looks that sometimes when you're most likely on the first screen the initial header height (taken from the `getDefaultHeaderHeight` method) stays and is not being updated from the `onHeaderHeightChange` event. Also, when user hides the status bar it wasn't counted to the final value of header height. This PR fixes those problems and also other minor issues, related to the calculating header height. ## Changes - Added calls for calculating header height on setting animated config and changing `statusBarHidden` prop. - Changed implementation of `getCalculatedStatusBarHeightIsModal` method. - Refactorized naming of the methods, related to the header height. - Added asserting modal hierarchy. ## Test code and steps to reproduce You can change `Modals.tsx` file by adding this snippet: ```js const headerHeight = useAnimatedHeaderHeight(); headerHeight.addListener((height) => console.log(height.value)) ``` to the components and listen to the changes in `Modals` example. Then try to hide header in modals - there should be `0` value as a header height. ## Checklist - [X] Included code example that can be used to test this change - [ ] Ensured that CI passes --- ios/RNSScreen.mm | 91 ++++++++++++++-------- ios/RNSScreenStack.mm | 7 +- ios/RNSScreenStackHeaderConfig.mm | 8 ++ src/native-stack/views/NativeStackView.tsx | 2 +- 4 files changed, 72 insertions(+), 36 deletions(-) diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index 99db2b5097..1ed4914729 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -251,6 +251,13 @@ - (void)setStatusBarHidden:(BOOL)statusBarHidden _statusBarHidden = statusBarHidden; [RNSScreenWindowTraits assertViewControllerBasedStatusBarAppearenceSet]; [RNSScreenWindowTraits updateStatusBarAppearance]; + + // As the status bar could change its visibility, we need to calculate header + // height for the correct value in `onHeaderHeightChange` event when navigation + // bar is not visible. + if (self.controller.navigationController.navigationBarHidden && !self.isModal) { + [self.controller calculateAndNotifyHeaderHeightChangeIsModal:NO]; + } } - (void)setScreenOrientation:(UIInterfaceOrientationMask)screenOrientation @@ -1031,6 +1038,12 @@ - (void)viewDidLayoutSubviews } } +- (BOOL)isModalWithHeader +{ + return self.screenView.isModal && self.childViewControllers.count == 1 && + [self.childViewControllers[0] isKindOfClass:UINavigationController.class]; +} + // Checks whether this screen has any child view controllers of type RNSNavigationController. // Useful for checking if this screen has nested stack or is displayed at the top. - (BOOL)hasNestedStack @@ -1044,33 +1057,9 @@ - (BOOL)hasNestedStack return NO; } -- (CGFloat)getCalculatedHeaderHeightIsModal:(BOOL)isModal -{ - CGFloat navbarHeight = self.navigationController.navigationBar.frame.size.height; - - // In case where screen is a modal, we want to calculate just its childViewController's height - if (isModal && self.childViewControllers.count > 0 && - [self.childViewControllers[0] isKindOfClass:UINavigationController.class]) { - UINavigationController *childNavCtr = self.childViewControllers[0]; - navbarHeight = childNavCtr.navigationBar.frame.size.height; - } - - return navbarHeight; -} - -- (CGSize)getCalculatedStatusBarHeightIsModal:(BOOL)isModal +- (CGSize)getStatusBarHeightIsModal:(BOOL)isModal { #if !TARGET_OS_TV - BOOL isDraggableModal = isModal && ![self.screenView isFullscreenModal]; - BOOL isDraggableModalWithChildViewCtr = - isDraggableModal && self.childViewControllers.count > 0 && self.childViewControllers[0] != nil; - - // When modal is floating (we can grab its header), we don't want to calculate status bar in it. - // Thus, we return '0' as a height of status bar. - if (isDraggableModalWithChildViewCtr || self.screenView.isTransparentModal) { - return CGSizeMake(0, 0); - } - CGSize fallbackStatusBarSize = [[UIApplication sharedApplication] statusBarFrame].size; #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ @@ -1087,21 +1076,57 @@ - (CGSize)getCalculatedStatusBarHeightIsModal:(BOOL)isModal #endif /* Check for iOS 13.0 */ #else - // On TVOS, status bar doesn't exist + // TVOS does not have status bar. return CGSizeMake(0, 0); #endif // !TARGET_OS_TV } +- (UINavigationController *)getVisibleNavigationControllerIsModal:(BOOL)isModal +{ + UINavigationController *navctr = self.navigationController; + + if (isModal) { + // In case where screen is a modal, we want to calculate childViewController's + // navigation bar height instead of the navigation controller from RNSScreen. + if (self.isModalWithHeader) { + navctr = self.childViewControllers[0]; + } else { + // If the modal does not meet requirements (there's no RNSNavigationController which means that probably it + // doesn't have header or there are more than one RNSNavigationController which is invalid) we don't want to + // return anything. + return nil; + } + } + + return navctr; +} + - (CGFloat)calculateHeaderHeightIsModal:(BOOL)isModal { - CGFloat navbarHeight = [self getCalculatedHeaderHeightIsModal:isModal]; - CGSize statusBarSize = [self getCalculatedStatusBarHeightIsModal:isModal]; + UINavigationController *navctr = [self getVisibleNavigationControllerIsModal:isModal]; + + // If navigation controller doesn't exists (or it is hidden) we want to handle two possible cases. + // If there's no navigation controller for the modal, we simply don't want to return header height, as modal possibly + // does not have header and we don't want to count status bar. If there's no navigation controller for the view we + // just want to return status bar height (if it's hidden, it will simply return 0). + if (navctr == nil || navctr.isNavigationBarHidden) { + if (isModal) { + return 0; + } else { + CGSize statusBarSize = [self getStatusBarHeightIsModal:isModal]; + return MIN(statusBarSize.width, statusBarSize.height); + } + } + + CGFloat navbarHeight = navctr.navigationBar.frame.size.height; +#if !TARGET_OS_TV + CGFloat navbarInset = navctr.navigationBar.frame.origin.y; +#else + // On TVOS there's no inset of navigation bar. + CGFloat navbarInset = 0; +#endif // !TARGET_OS_TV - // Unfortunately, UIKit doesn't care about switching width and height options on screen rotation. - // We should check if user has rotated its screen, so we're choosing the minimum value between the - // width and height. - CGFloat statusBarHeight = MIN(statusBarSize.width, statusBarSize.height); - return navbarHeight + statusBarHeight; + return navbarHeight + navbarInset; } - (void)calculateAndNotifyHeaderHeightChangeIsModal:(BOOL)isModal diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index 61fe072086..7961d5360d 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -66,12 +66,15 @@ - (void)viewDidLayoutSubviews [super viewDidLayoutSubviews]; if ([self.topViewController isKindOfClass:[RNSScreen class]]) { RNSScreen *screenController = (RNSScreen *)self.topViewController; + BOOL isNotDismissingModal = screenController.presentedViewController == nil || + (screenController.presentedViewController != nil && + ![screenController.presentedViewController isBeingDismissed]); // Calculate header height during simple transition from one screen to another. // If RNSScreen includes a navigation controller of type RNSNavigationController, it should not calculate // header height, as it could have nested stack. - if (![screenController hasNestedStack]) { - [(RNSScreen *)self.topViewController calculateAndNotifyHeaderHeightChangeIsModal:NO]; + if (![screenController hasNestedStack] && isNotDismissingModal) { + [screenController calculateAndNotifyHeaderHeightChangeIsModal:NO]; } } } diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index f88ca647bc..4fa7506285 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -147,6 +147,9 @@ - (void)updateViewControllerIfNeeded // if nav is nil, it means we can be in a fullScreen modal, so there is no nextVC, but we still want to update if (vc != nil && (nextVC == vc || isInFullScreenModal || isPresentingVC)) { [RNSScreenStackHeaderConfig updateViewController:self.screenView.controller withConfig:self animated:YES]; + // As the header might have change in `updateViewController` we need to ensure that header height + // returned by the `onHeaderHeightChange` event is correct. + [self.screenView.controller calculateAndNotifyHeaderHeightChangeIsModal:NO]; } } @@ -353,6 +356,11 @@ + (void)willShowViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config { [self updateViewController:vc withConfig:config animated:animated]; + // As the header might have change in `updateViewController` we need to ensure that header height + // returned by the `onHeaderHeightChange` event is correct. + if ([vc isKindOfClass:[RNSScreen class]]) { + [(RNSScreen *)vc calculateAndNotifyHeaderHeightChangeIsModal:NO]; + } } #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ diff --git a/src/native-stack/views/NativeStackView.tsx b/src/native-stack/views/NativeStackView.tsx index 43768aae92..6e75c79d1d 100644 --- a/src/native-stack/views/NativeStackView.tsx +++ b/src/native-stack/views/NativeStackView.tsx @@ -252,7 +252,7 @@ const RouteView = ({ // We need to ensure the first retrieved header height will be cached and set in animatedHeaderHeight. // We're caching the header height here, as on iOS native side events are not always coming to the JS on first notify. // TODO: Check why first event is not being received once it is cached on the native side. - const cachedAnimatedHeaderHeight = React.useRef(statusBarHeight); + const cachedAnimatedHeaderHeight = React.useRef(defaultHeaderHeight); const animatedHeaderHeight = React.useRef( new Animated.Value(staticHeaderHeight, { useNativeDriver: true,