Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions apps/src/tests/Test3173.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as React from 'react';
import { View, Text, Button } from 'react-native';
import { Screen, ScreenStack, ScreenStackHeaderConfig } from '../../../src';
import { useState } from 'react';
import Colors from '../shared/styling/Colors';

function HomeScreen({ add }: { add: () => void }) {
console.log('Render home');
return (
<View
style={{
flex: 1,
gap: 8,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Colors.RedLight40,
}}>
<Text>Home!</Text>
<Button
title="Next"
onPress={add}
/>
</View>
);
}

function ProfileScreen({ add, idx }: { add: () => void, idx: number }) {
const colors = [Colors.BlueLight40, Colors.GreenLight40, Colors.YellowLight40];
const [colorIndex, setColorIndex] = useState(0);
console.log('Render another');
return (
<View
style={{
flex: 1,
gap: 8,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors[colorIndex]
}}>
<Text>Another! #{idx}</Text>
<Button
title="More"
onPress={add}
/>
<Button
title="Recolor"
onPress={() => setColorIndex(i => (i+1) % 3)}
/>
</View>
);
}

const App = () => {
const [count, setCount] = React.useState(0);

return (
<ScreenStack style={{ flex: 1 }}>
<Screen key='home' activityState={2} isNativeStack onAppear={() => {setCount(0)}}>
<ScreenStackHeaderConfig title='Home'/>
<HomeScreen add={() => setCount(n => n+1)}/>
</Screen>
{ Array.from({length: count}).map((_, i) => (
<Screen key={`prof${i}`} activityState={2} isNativeStack>
<ScreenStackHeaderConfig title={'Another #' + (i+1)}/>
<ProfileScreen add={() => setCount(n => n+1)} idx={i+1}/>
</Screen>
)) }
</ScreenStack>
);
};
export default App;
1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export { default as Test3093 } from './Test3093';
export { default as Test3111 } from './Test3111';
export { default as Test3115 } from './Test3115';
export { default as Test3168 } from './Test3168';
export { default as Test3173 } from './Test3173';
export { default as TestScreenAnimation } from './TestScreenAnimation';
export { default as TestScreenAnimationV5 } from './TestScreenAnimationV5';
export { default as TestHeader } from './TestHeader';
Expand Down
4 changes: 4 additions & 0 deletions guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,15 @@ Defaults to `false`. When `enableFreeze()` is run at the top of the application
### `fullScreenSwipeEnabled` (iOS only)

Boolean indicating whether the swipe gesture should work on whole screen. Swiping with this option results in the same transition animation as `simple_push` by default. It can be changed to other custom animations with `customAnimationOnSwipe` prop, but default iOS swipe animation is not achievable due to usage of custom recognizer. Defaults to `false`.
IMPORTANT: Starting from iOS 26, full screen swipe is handled by native recognizer, and this prop is ignored.

### `fullScreenSwipeShadowEnabled` (iOS only)

Boolean indicating whether the full screen dismiss gesture has shadow under view during transition. The gesture uses custom transition and thus
doesn't have a shadow by default. When enabled, a custom shadow view is added during the transition which tries to mimic the
default iOS shadow. Defaults to `true`.
IMPORTANT: Starting from iOS 26, full screen swipe is handled by native recognizer, and this prop is ignored. We still fallback
to the legacy implementation when when handling custom animations, but we assume `true` for shadows.

### `gestureEnabled` (iOS only)

Expand All @@ -69,6 +72,7 @@ When set to `false` the back swipe gesture will be disabled. The default value i
#### `gestureResponseDistance` (iOS only)

Use it to restrict the distance from the edges of screen in which the gesture should be recognized. To be used alongside `fullScreenSwipeEnabled`. The responsive area is covered with 4 values: `start`, `end`, `top`, `bottom`. Example usage:
IMPORTANT: Starting from iOS 26, this prop conflicts with the native behavior of full screen swipe to dismiss, therefore it is ignored.

```tsx
gestureResponseDistance: {
Expand Down
49 changes: 47 additions & 2 deletions ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,18 @@ - (void)initCommonProps
#endif // RCT_NEW_ARCH_ENABLED
}

- (BOOL)getFullScreenSwipeShadowEnabled
{
if (@available(iOS 26, *)) {
// fullScreenSwipeShadow is tied to RNSPanGestureRecognizer, which, on iOS 26, is used only for custom animations,
// and replaced with native interactiveContentPopGestureRecognizer for everything else.
// We want them to look similar and native-like, so it should default to `YES`.
return YES;
}

return _fullScreenSwipeShadowEnabled;
}

- (UIViewController *)reactViewController
{
return _controller;
Expand Down Expand Up @@ -730,9 +742,27 @@ - (void)notifyTransitionProgress:(double)progress closing:(BOOL)closing goingFor
#endif
}

#if !RCT_NEW_ARCH_ENABLED
- (void)willMoveToWindow:(UIWindow *)newWindow
{
if (@available(iOS 26, *)) {
// In iOS 26, as soon as another screen appears in transition, it is interactable
// To avoid glitches resulting from clicking buttons mid transition, we temporarily disable all interactions
// Disabling interactions for parent navigation controller won't be enough in case of nested stack
// Furthermore, a stack put inside a modal will exist in an entirely different hierarchy
// To be sure, we block interactions on the whole window.
// Note that newWindows is nil when moving from instead of moving to, and Obj-C handles nil correctly
newWindow.userInteractionEnabled = false;
}
}

- (void)presentationControllerWillDismiss:(UIPresentationController *)presentationController
{
if (@available(iOS 26, *)) {
// Disable interactions to disallow multiple modals dismissed at once; see willMoveToWindow
presentationController.containerView.window.userInteractionEnabled = false;
}

#if !RCT_NEW_ARCH_ENABLED
// On Paper, we need to call both "cancel" and "reset" here because RN's gesture
// recognizer does not handle the scenario when it gets cancelled by other top
// level gesture recognizer. In this case by the modal dismiss gesture.
Expand All @@ -745,8 +775,8 @@ - (void)presentationControllerWillDismiss:(UIPresentationController *)presentati
// down.
[_touchHandler cancel];
[_touchHandler reset];
}
#endif // !RCT_NEW_ARCH_ENABLED
}

- (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presentationController
{
Expand All @@ -758,6 +788,11 @@ - (BOOL)presentationControllerShouldDismiss:(UIPresentationController *)presenta

- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)presentationController
{
if (@available(iOS 26, *)) {
// Reenable interactions; see presentationControllerWillDismiss
presentationController.containerView.window.userInteractionEnabled = true;
}

// NOTE(kkafar): We should consider depracating the use of gesture cancel here & align
// with usePreventRemove API of react-navigation v7.
[self notifyGestureCancel];
Expand All @@ -768,6 +803,12 @@ - (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)pr

- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
if (@available(iOS 26, *)) {
// Reenable interactions; see presentationControllerWillDismiss
// Dismissed screen doesn't hold a reference to window, but presentingViewController.view does
presentationController.presentingViewController.view.window.userInteractionEnabled = true;
}

if ([_reactSuperview respondsToSelector:@selector(presentationControllerDidDismiss:)]) {
[_reactSuperview performSelector:@selector(presentationControllerDidDismiss:) withObject:presentationController];
}
Expand Down Expand Up @@ -1506,6 +1547,10 @@ - (void)viewWillDisappear:(BOOL)animated

- (void)viewDidAppear:(BOOL)animated
{
if (@available(iOS 26, *)) {
// Reenable interactions, see willMoveToWindow
self.view.window.userInteractionEnabled = true;
}
[super viewDidAppear:animated];
if (!_isSwiping || _shouldNotify) {
// we are going forward or dismissing without swipe
Expand Down
Loading
Loading