Skip to content

Conversation

kmichalikk
Copy link
Collaborator

@kmichalikk kmichalikk commented Aug 29, 2025

Description

Resolves https://github.com/software-mansion/react-native-screens-labs/issues/369, might resolve #3161, reverts #3141, #3142

This PR attempts to enable interactiveContentPopGestureRecognizer for iOS 26 to achieve native screen popping behavior. Until 26, the default was to swipe from the edge of the screen. We had the option to do fullscreen switch, which was controlled by a fullScreenSwipeEnabled prop. Since the default behavior has changed, this prop, along with gestureResponseDistance, is being ignored from now on.

New iOS allows for popping multiple screens almost at once, which we still cannot support due to asynchronous nature of stack updates coming from host to JS that would create a "feedback loop" in situation like the following: host pops 1. screen + sends update, pops 2. screen + sends update -> JS acknowledges 1. update + sends updated state -> host gets 2. screen from JS and pushes it again. This PR attempts to block more than 1 pop at once by removing interactions from the whole screen. As a (desired) side effect, this also disables interactions on the screen below the one that is popped until the transition finishes.

Changes

  • removed RNSPanGestureRecognizer from iOS 26 build and replace it with native interactiveContentPopGestureRecognizer
  • disabled all interactions when screens are in transition
  • updated docs

Test code and steps to reproduce

Use Test3173 to test swipe and interactions on bare screens API & compare with any other test that uses react-navigation stack, i.e Test3093. Use Test3093 with additional screenOptions:

{
        animation: 'slide_from_bottom',
        animationMatchesGesture: true,
}

to test custom animations on swipe back.

@kmichalikk kmichalikk force-pushed the @kmichalikk/disable-screen-interaction-on-transition branch from 9a80b45 to 468b1fe Compare September 2, 2025 06:16
@kmichalikk kmichalikk marked this pull request as ready for review September 2, 2025 07:05
ios/RNSScreen.mm Outdated
Comment on lines 1539 to 1544
if (@available(iOS 26, *)) {
// Reenable interactions, see viewWillAppear
[self findReactRootViewController].view.userInteractionEnabled = true;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking, is it possible that viewWillAppear will run but viewDidAppear will not? Did you check how prevent remove works now?

Copy link
Collaborator Author

@kmichalikk kmichalikk Sep 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this on Test2125 which demos preventRemove

When using back button with prevent remove, the calls look like this:
image
We are covered by SecondScreen going back on top

When using swipe and not dismissing fully:
image
It's the same

When using swipe and actually dismissing SecondScreen (preventRemove still on):
image
Which is different ( 🤔 ) but works for our case

and without preventRemove it's like this:
image

so in every case at least one of the screens goes through the whole process

@kmichalikk kmichalikk force-pushed the @kmichalikk/disable-screen-interaction-on-transition branch from 08ccb30 to 267ff70 Compare September 3, 2025 06:01
@kmichalikk kmichalikk force-pushed the @kmichalikk/disable-screen-interaction-on-transition branch from 267ff70 to 852ed1f Compare September 3, 2025 06:19
@kmichalikk kmichalikk requested a review from kligarski September 3, 2025 06:23
Copy link
Contributor

@kligarski kligarski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also check behavior with stack nested in modal, as it is attached directly to UIWindow instead of react root view (@WoLewicki's suggestion).

@kmichalikk kmichalikk requested a review from kligarski September 3, 2025 13:08
ios/RNSScreen.mm Outdated
// Furthermore, a stack put inside a modal will exist in an entirely different hierarchy
// To be sure, we block interactions on the whole window
// We need to get the window instance from parent view because it is not set until the view has appeared
self.parentViewController.view.window.userInteractionEnabled = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check one more thing - when you have some push screens and opened modal, when you dismiss the modal and quickly click the back button, will it work correctly (I'm not sure if viewWillAppear will be called)?

Copy link
Contributor

@kligarski kligarski Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And another thing - when I tested main (previous solution), this happened:

Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-09-04.at.10.44.06.mov

Please check if this solution prevents this but I managed to also reproduce the bug on iOS 18. If this isn't fixed by this PR, let's create a ticket to investigate this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when you dismiss the modal and quickly click the back button

It actually broke again in situation: stack1_screenA / stack2_screen A / stack2_screen B / stack2_modal C; when quickly dismissing modal and screen, the whole stack was dismissed. I managed to fix that, but it's not pretty. The main challenge was to find a reference to correct window without using sharedApplication.

As for dismissing multiple modals, native UIKit prevents dismissing more than 2 at once, blocking the third one mid swipe. I ended up blocking every consecutive dismissal similar to regular screens.

And another thing - when I tested main (previous solution), this happened:

I didn't notice anything on my PR

@kmichalikk
Copy link
Collaborator Author

Paper appears to work, too.

One other thing I found is that when UIWindow has interactions disabled and we still try to input gestures, this log is shown

Unexpected nil window in latent system gesture client update: windowServerHitTestWindow: <UIWindow: 0x101b05640; frame = (0 0; 402 874); autoresize = W+H; gestureRecognizers = <NSArray: 0x600001829800>; layer = <UIWindowLayer: 0x60000171f100>>, touch: <UITouch: 0x10e788d40> type: Direct; phase: Stationary; is pointer: NO; tap count: 1; force: 0.000; window: (null); responder: (null);

I suspect that's because there is nothing above the window to handle the gesture, but I don't know really. There is not that much info online, other that people having this error when app breaks with black screen, which doesn't happen here.

@kmichalikk kmichalikk requested a review from kligarski September 5, 2025 09:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Xcode 26] fullScreenGestureEnabled cause bug on scroll
2 participants