Skip to content
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

Incorrect layout when navigating between screens of different orientation on iOS #1738

Closed
thomas-coldwell opened this issue Mar 14, 2023 · 16 comments · Fixed by #1970
Closed
Assignees
Labels
Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snack or repo is provided

Comments

@thomas-coldwell
Copy link

Description

This issue is hard to consistently reproduce, however, when navigating between screens with a different orientation e.g. Landscape -> Portrait, the new screen is incorrectly laid out with the previous screen's orientation - see screenshot below of the view hierarchy after pushing a new portrait screen.

Screenshot 2023-03-14 at 16 06 14

Simulator.Screen.Recording.-.iPhone.14.-.2023-03-14.at.18.29.56.mp4

As you can see the RNSScreenView (child of the RNSScreen UIViewController) that I've navigated to has had its frame set to landscape dimensions even though it is actually a portrait screen, resulting in a broken layout.

I've added a log to the setFrame method of the RNSScreenView to see where this gets called from and with what dimensions (if you scroll to the right you can see the calling method):

// When it breaks ❌:
2023-03-12 20:41:11.536827+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 0.000000x0.000000 called from 2   UIKitCore                           0x0000000131f58325 UIViewCommonInitWithFrame + 1849
2023-03-12 20:41:11.796876+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 844.000000x390.000000 called from 2   UIKitCore                           0x0000000130f725fa -[UINavigationController _startCustomTransition:] + 1348
2023-03-12 20:41:11.809961+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 844.000000x390.000000 called from 2   OrientationTest                            0x0000000104cdeff8 -[RNSScreenStackAnimator animateTransition:] + 392
2023-03-12 20:41:11.823229+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 844.000000x390.000000 called from 2   OrientationTest                            0x0000000104ce0aa2 -[RNSScreenStackAnimator animateFadeWithTransitionContext:toVC:fromVC:] + 338

// When it lays out correctly ✅:
2023-03-12 20:43:29.145651+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 0.000000x0.000000 called from 2   UIKitCore                           0x0000000131f58325 UIViewCommonInitWithFrame + 1849
2023-03-12 20:43:29.401373+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 844.000000x390.000000 called from 2   UIKitCore                           0x0000000130f725fa -[UINavigationController _startCustomTransition:] + 1348
2023-03-12 20:43:29.408939+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 844.000000x390.000000 called from 2   OrientationTest                            0x0000000104cdeff8 -[RNSScreenStackAnimator animateTransition:] + 392
2023-03-12 20:43:29.417094+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 844.000000x390.000000 called from 2   OrientationTest                            0x0000000104ce0aa2 -[RNSScreenStackAnimator animateFadeWithTransitionContext:toVC:fromVC:] + 338
2023-03-12 20:43:29.519909+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 844.000000x844.000000 called from 2   UIKitCore                           0x0000000130f89f46 -[UINavigationController _layoutViewController:] + 1194
2023-03-12 20:43:29.820028+0000 OrientationTest[49171:3980390] 👉RNSScreenView setting frame to 390.000000x844.000000 called from 2   UIKitCore                           0x0000000130f89f46 -[UINavigationController _layoutViewController:] + 1194

There are a couple of interesting things to note in the logs above:

  • Firstly, the [UINavigationController _startCustomTransition:] & related transition animation methods set the new screen to landscape dimensions (even though the screen is specified as portrait). It looks like this is due to the fact the UINavigationController's supportedInterfaceOrientations are specified by its top view controller which in this case would still be landscape (before the transition has started and before the new view controllers have been set) & then the UINavigationController internally sets the final frame for the view we are transitioning to based on this in its various animated transition methods.
  • Secondly, in the case where the layout is correct, there is a final [UINavigationController _layoutViewController:] presumably from it some other update to the navigation controller triggering a re-layout with the correct, portrait dimensions for the screen.

Expected Behaviour

That when navigating to a portrait screen it should correctly layout with portrait dimensions.

Steps to reproduce

  1. Open example on an iPhone simulator (we've been able to reproduce it on a physical device, but its most easily reproducible here for some reason)
  2. Navigate back and forth between Landscape -> Portrait screens
  3. Observe the calls to setFrame in the RNSScreenView
  4. Eventually the layout should break as shown in the above screenshot (this might take a number of tries)
  5. See that the Portrait screen is laid out with Landscape dimensions in the XCode view hierarchy debugger

Snack or a link to a repository

https://github.com/thomas-coldwell/rnscreen-orientation-glitch

Screens version

3.20.0

React Native version

0.71.3

Platforms

iOS

JavaScript runtime

Hermes

Workflow

React Native (without Expo)

Architecture

Paper (Old Architecture)

Build type

Debug mode

Device

iOS simulator

Device model

iPhone 14

Acknowledgements

Yes

@github-actions github-actions bot added Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snack or repo is provided labels Mar 14, 2023
@kkafar
Copy link
Member

kkafar commented Mar 15, 2023

Hi @thomas-coldwell, thanks for reporting.
Would you confirm that the issue does not appear when running on iOS < 16 (try out iPhone 13)?

Also, do you see message from UIKit as in #1732?

@kkafar kkafar self-assigned this Mar 15, 2023
@thomas-coldwell
Copy link
Author

I'm downloading iOS 15.5 to test out on the simulator now and will report if the issue exists there. I can confirm I do see the UIKit requestGeometryUpdate message you mentioned in that other PR

@kkafar
Copy link
Member

kkafar commented Mar 15, 2023

Thank you!

@thomas-coldwell
Copy link
Author

thomas-coldwell commented Mar 15, 2023

I can confirm this issue does not occur on iOS 15.5 (tested on an iPhone 13 Pro Max & iPhone 8 simulator just to make sure it wasn't any weird screen size issue). I'll also test with the requestGeometryUpdate changes on an iOS 16 simulator and see if that resolves the issue in my reproduction app

@thomas-coldwell
Copy link
Author

thomas-coldwell commented Mar 15, 2023

I've just tested it on iOS 16 (iPhone 14 simulator) again with the changes for the requestGeometryUpdate in #1732 but the issue is still present

@thomas-coldwell
Copy link
Author

Also just another bit of info to add to this that might help us solve it - viewWillTransitionToSize on the UINavigationController always reports the correct dimensions of the view its transitioning to. This might be helpful if we need to force the size of the view we are transitioning to

@vinkim
Copy link

vinkim commented Mar 17, 2023

Thanks for the info and the reproduction repo Thomas!

I ended up spending a bit of time trying to debug this, trying to follow which code paths calls what.

For my case, I ended up with a temporary workaround, in which case I was not able to reproduce the issue in your reproduction repo.

I added the following line to RNSScreenStack.mm, in your reproduction repo

- (void)navigationController:(UINavigationController *)navigationController
       didShowViewController:(UIViewController *)viewController
                    animated:(BOOL)animated
{
  [self emitOnFinishTransitioningEvent];
  [RNSScreenWindowTraits updateWindowTraits];
  [_controller.view setNeedsLayout]; <------- This line
}

Since this was enough to solve our use case by doing a patch-package, I didn't debug any further as of now.

@thomas-coldwell
Copy link
Author

That works really well @vinkim! I think I tried this before (outside the sandbox project) and it didn't perform the layout, but will re-test with this! Thanks for helping to debug it 🙌

@Thenlie
Copy link

Thenlie commented Mar 17, 2023

I am experiencing a very similar issue, though slightly different I think. This all started after switching to the NativeStackView from StackView.

Screens render well in either orientation, but on one screen if you rotate, it is not updated to the new layout. I have tired manually setting the height and width based on the device window dimensions, but that does not make a change. I also tried the patch suggested by @vinkim which also did not solve the issue.

Trying to make a small repro case but don't have the time just yet.

image

Currently testing on:
iPad 10th Gen, iOS 16.1.
React Native 0.67.5
React Native Screens 3.19.0

@Salmankhan033
Copy link

I have the same issue, Is someone fixed it?

@feedthedevil
Copy link

Thanks for the info and the reproduction repo Thomas!

I ended up spending a bit of time trying to debug this, trying to follow which code paths calls what.

For my case, I ended up with a temporary workaround, in which case I was not able to reproduce the issue in your reproduction repo.

I added the following line to RNSScreenStack.mm, in your reproduction repo

- (void)navigationController:(UINavigationController *)navigationController
       didShowViewController:(UIViewController *)viewController
                    animated:(BOOL)animated
{
  [self emitOnFinishTransitioningEvent];
  [RNSScreenWindowTraits updateWindowTraits];
  [_controller.view setNeedsLayout]; <------- This line
}

Since this was enough to solve our use case by doing a patch-package, I didn't debug any further as of now.

Thank you! Works for me :)

@madox2
Copy link

madox2 commented Oct 18, 2023

I am facing a similar issue on iOS 16.7.1. My setup is slightly more complicated, I control orientation manually with expo-screen-orientation package. This became issue once I introduced nested Tab navigator into root Stack navigator. It seems to be working fine when I disable animation on the landscape screen:

<Stack.Screen
  name="landscape-full-screen"
  component={LandscapeFullScreen}
  options={{
    animation: 'none',
   // ...

Packages:

"react-native-screens": "~3.22.0",
"@react-navigation/native": "^6.1.8",
"@react-navigation/native-stack": "^6.9.14",
"@react-navigation/bottom-tabs": "^6.5.9",
"expo-screen-orientation": "~6.0.5",

@tboba
Copy link
Member

tboba commented Nov 15, 2023

Hey @thomas-coldwell, @vinkim (huge shout out to you for suggesting the change!), @Thenlie, @Salmankhan033, @feedthedevil, @madox2! Sorry for the late response in this issue 😅

I've created the PR with the suggested by @vinkim changes. I've also came to another solution that you can find in the PR, but I believe setNeedsLayout should be sufficient here. Could you test if this change fixes the problem for you?

You can check those changes by changing line with react-native-screens dependency in your package.json:

"react-native-screens": "software-mansion/react-native-screens#@tboba/fix-updating-bounds"

tboba added a commit that referenced this issue Nov 15, 2023
As I've read in #1738 that this issue is also reproducible on the old architecture (but it's hard to reproduce it there) I'm reverting this change just in case if this problem could also occur there.
@nateshmbhat
Copy link

any idea when this fix will get merged and released ?

@yahacom
Copy link

yahacom commented Apr 1, 2024

Hello! I'm also interesting when it'll be fixed.
Actually fix suggested by @vinkim help me make it work.

But I still can see this ruge UI transformations when layout transforms from landscape to portrait and vice versa. It looks like layout transforms first and only after it apply new width and height (and app layout looks broken for a part of second in a middle).
So, it still need some fix to make transition more smooth.

tboba added a commit that referenced this issue May 8, 2024
…1970)

## Description

Currently while trying to change the interface orientation there's a bug
that doesn't layout the next screen, causing the old frame to remain
(which means that the next screen will be rotated regarding to the
previous orientation.
This PR fixes that problem by calling `[_controller.view
setNeedsLayout]` on the `navigationController`, where the view is being
rotated.

When I was testing this PR I also came to another solution of setting
the frame in `viewWillTransitionToSize` but I didn't spot any
differences between one solution and this one.

```objc
- (void)viewWillTransitionToSize:(CGSize)size
       withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
#ifdef RCT_NEW_ARCH_ENABLED
  [coordinator
      animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> coordinator) {
        self.screenView.frame = CGRectMake(0, 0, size.width, size.height);
      }
      completion:^(id<UIViewControllerTransitionCoordinatorContext> coordinator) {
        [self.screenView setNeedsLayout];
      }];
#endif
}
```

I've also tried to make a workaround of a reaaaally old bug in React
Native: facebook/react-native#16060, but
unfortunately I failed with that 😕

Closes #1738.

## Changes

- Adds call to `setNeedsLayout` during the interface orientation

## Screenshots / GIFs

### Before


https://github.com/software-mansion/react-native-screens/assets/23281839/bcc366ab-9757-4a31-89e0-241c22cdd5c6

### After


https://github.com/software-mansion/react-native-screens/assets/23281839/6634379a-2697-46a3-aed6-3cb156616817

## Test code and steps to reproduce

You can find in this PR `Test1970.tsx` that contains test test I've
tested while making this change.

## Checklist

- [X] Included code example that can be used to test this change
- [x] Ensured that CI passes
@yahacom
Copy link

yahacom commented May 28, 2024

Unfortunately, I'm still experiencing the same issue on the latest v3.31.1 - the weird transition between portrait and landscape and vice versa on iOS. At same time it works smoothly on Android.
Sometimes on iOS transition from landscape to portrait is not finished at all. It looks like screen was rotated but still keeps old dimensions for width and height.

I'm using

"react-native": "0.74.1",
"@react-navigation/drawer": "^6.6.15",
"@react-navigation/native": "^6.1.17",
"@react-navigation/native-stack": "^6.9.26",

I'm trying to make it work in two ways:

  • with the orientation option of Native Stack Navigator
  • with help of react-native-orientation-locker lib (v. 1.5.0)

I tried to add animation: 'none' to native stack screen options as @madox2 suggested, it helps a little bit, but transition is still not smooth.

ja1ns pushed a commit to WiseOwlTech/react-native-screens that referenced this issue Oct 9, 2024
…oftware-mansion#1970)

## Description

Currently while trying to change the interface orientation there's a bug
that doesn't layout the next screen, causing the old frame to remain
(which means that the next screen will be rotated regarding to the
previous orientation.
This PR fixes that problem by calling `[_controller.view
setNeedsLayout]` on the `navigationController`, where the view is being
rotated.

When I was testing this PR I also came to another solution of setting
the frame in `viewWillTransitionToSize` but I didn't spot any
differences between one solution and this one.

```objc
- (void)viewWillTransitionToSize:(CGSize)size
       withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
  [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
#ifdef RCT_NEW_ARCH_ENABLED
  [coordinator
      animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> coordinator) {
        self.screenView.frame = CGRectMake(0, 0, size.width, size.height);
      }
      completion:^(id<UIViewControllerTransitionCoordinatorContext> coordinator) {
        [self.screenView setNeedsLayout];
      }];
#endif
}
```

I've also tried to make a workaround of a reaaaally old bug in React
Native: facebook/react-native#16060, but
unfortunately I failed with that 😕

Closes software-mansion#1738.

## Changes

- Adds call to `setNeedsLayout` during the interface orientation

## Screenshots / GIFs

### Before


https://github.com/software-mansion/react-native-screens/assets/23281839/bcc366ab-9757-4a31-89e0-241c22cdd5c6

### After


https://github.com/software-mansion/react-native-screens/assets/23281839/6634379a-2697-46a3-aed6-3cb156616817

## Test code and steps to reproduce

You can find in this PR `Test1970.tsx` that contains test test I've
tested while making this change.

## Checklist

- [X] Included code example that can be used to test this change
- [x] Ensured that CI passes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Platform: iOS This issue is specific to iOS Repro provided A reproduction with a snack or repo is provided
Projects
None yet
10 participants