Skip to content

Commit

Permalink
fix(iOS): change implementation of calculating status bar, refactor m…
Browse files Browse the repository at this point in the history
…ethods 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
  • Loading branch information
tboba authored Oct 17, 2023
1 parent 20fccf1 commit dbb7430
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 36 deletions.
91 changes: 58 additions & 33 deletions ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) && \
Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions ios/RNSScreenStack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions ios/RNSScreenStackHeaderConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}

Expand Down Expand Up @@ -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) && \
Expand Down
2 changes: 1 addition & 1 deletion src/native-stack/views/NativeStackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit dbb7430

Please sign in to comment.