From 027a3a6d119437a7f96e55126f2f7631405df95e Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Wed, 24 Sep 2025 07:07:41 +0200 Subject: [PATCH 1/9] Use OptionalBoolean to handle different default values for fullScreenSwipeEnabled --- .../RNSScreenManagerDelegate.java | 2 +- .../RNSScreenManagerInterface.java | 2 +- ios/RNSConvert.h | 3 +++ ios/RNSConvert.mm | 14 +++++++++++++ ios/RNSScreen.h | 4 +++- ios/RNSScreen.mm | 20 +++++++++++++++++-- ios/RNSScreenStack.mm | 19 +++++++++--------- ios/RNSScreenStackAnimator.mm | 2 +- src/components/Screen.tsx | 8 ++++++++ src/fabric/ModalScreenNativeComponent.ts | 4 +++- src/fabric/ScreenNativeComponent.ts | 4 +++- 11 files changed, 65 insertions(+), 17 deletions(-) diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java index 83182f73ee..0217c1ebc5 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerDelegate.java @@ -53,7 +53,7 @@ public void setProperty(T view, String propName, @Nullable Object value) { mViewManager.setCustomAnimationOnSwipe(view, value == null ? false : (boolean) value); break; case "fullScreenSwipeEnabled": - mViewManager.setFullScreenSwipeEnabled(view, value == null ? false : (boolean) value); + mViewManager.setFullScreenSwipeEnabled(view, (String) value); break; case "fullScreenSwipeShadowEnabled": mViewManager.setFullScreenSwipeShadowEnabled(view, value == null ? true : (boolean) value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java index 4ac420a335..80a3346116 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenManagerInterface.java @@ -25,7 +25,7 @@ public interface RNSScreenManagerInterface { void setSheetInitialDetent(T view, int value); void setSheetElevation(T view, int value); void setCustomAnimationOnSwipe(T view, boolean value); - void setFullScreenSwipeEnabled(T view, boolean value); + void setFullScreenSwipeEnabled(T view, @Nullable String value); void setFullScreenSwipeShadowEnabled(T view, boolean value); void setHomeIndicatorHidden(T view, boolean value); void setPreventNativeDismiss(T view, boolean value); diff --git a/ios/RNSConvert.h b/ios/RNSConvert.h index 6be86d6f1f..b3cfe62202 100644 --- a/ios/RNSConvert.h +++ b/ios/RNSConvert.h @@ -18,6 +18,9 @@ namespace react = facebook::react; + (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent: (react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode; ++ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent: + (react::RNSScreenFullScreenSwipeEnabled)fullScreenSwipeEnabled; + + (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent: (react::RNSScreenStackPresentation)stackPresentation; diff --git a/ios/RNSConvert.mm b/ios/RNSConvert.mm index 35890ff48a..3647b2f824 100644 --- a/ios/RNSConvert.mm +++ b/ios/RNSConvert.mm @@ -36,6 +36,20 @@ + (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFr } } ++ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent: + (react::RNSScreenFullScreenSwipeEnabled)fullScreenSwipeEnabled +{ + switch (fullScreenSwipeEnabled) { + using enum react::RNSScreenFullScreenSwipeEnabled; + case Undefined: + return RNSOptionalBooleanUndefined; + case True: + return RNSOptionalBooleanTrue; + case False: + return RNSOptionalBooleanFalse; + } +} + + (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent: (react::RNSScreenStackPresentation)stackPresentation { diff --git a/ios/RNSScreen.h b/ios/RNSScreen.h index e15d2c03b2..52c2c99693 100644 --- a/ios/RNSScreen.h +++ b/ios/RNSScreen.h @@ -72,7 +72,7 @@ namespace react = facebook::react; RNSSafeAreaProviding, RNSScrollEdgeEffectProviding> -@property (nonatomic) BOOL fullScreenSwipeEnabled; +@property (nonatomic) RNSOptionalBoolean fullScreenSwipeEnabled; @property (nonatomic) BOOL fullScreenSwipeShadowEnabled; @property (nonatomic) BOOL gestureEnabled; @property (nonatomic) BOOL hasStatusBarHiddenSet; @@ -164,6 +164,8 @@ namespace react = facebook::react; - (BOOL)isModal; - (BOOL)isPresentedAsNativeModal; +- (BOOL)isFullScreenSwipeBackEnabled; + /** * Tell `Screen` component that it has been removed from react state and can safely cleanup * any retained resources. diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index c0801a3605..870e3dbc8d 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -891,6 +891,21 @@ - (BOOL)isPresentedAsNativeModal return self.controller.parentViewController == nil && self.controller.presentingViewController != nil; } +- (BOOL)isFullScreenSwipeBackEnabled +{ + switch (self.fullScreenSwipeEnabled) { + case RNSOptionalBooleanTrue: + return YES; + case RNSOptionalBooleanFalse: + return NO; + case RNSOptionalBooleanUndefined: + if (@available(iOS 26, *)) { + return YES; + } + return NO; + } +} + - (BOOL)isFullscreenModal { switch (self.controller.modalPresentationStyle) { @@ -1333,7 +1348,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: const auto &oldScreenProps = *std::static_pointer_cast(_props); const auto &newScreenProps = *std::static_pointer_cast(props); - [self setFullScreenSwipeEnabled:newScreenProps.fullScreenSwipeEnabled]; + [self setFullScreenSwipeEnabled:[RNSConvert RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent: + newScreenProps.fullScreenSwipeEnabled]]; [self setFullScreenSwipeShadowEnabled:newScreenProps.fullScreenSwipeShadowEnabled]; @@ -2171,7 +2187,7 @@ @implementation RNSScreenManager // we want to handle the case when activityState is nil RCT_REMAP_VIEW_PROPERTY(activityState, activityStateOrNil, NSNumber) RCT_EXPORT_VIEW_PROPERTY(customAnimationOnSwipe, BOOL); -RCT_EXPORT_VIEW_PROPERTY(fullScreenSwipeEnabled, BOOL); +RCT_EXPORT_VIEW_PROPERTY(fullScreenSwipeEnabled, RNSOptionalBoolean); RCT_EXPORT_VIEW_PROPERTY(fullScreenSwipeShadowEnabled, BOOL); RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(gestureResponseDistance, NSDictionary) diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index ba8dcc4949..5adbe88efd 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -1198,24 +1198,25 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) if (@available(iOS 26, *)) { - // On iOS 26, fullScreenSwipeEnabled takes no effect, and depending on whether custom animations are on, - // we select either interactiveContentPopGestureRecognizer or RNSPanGestureRecognizer - if (([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]] && - !customAnimationOnSwipePropSetAndSelectedAnimationIsCustom) || - (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer && - customAnimationOnSwipePropSetAndSelectedAnimationIsCustom)) { - return NO; + // On iOS 26, depending on whether custom animations are on, we select + // either interactiveContentPopGestureRecognizer or RNSPanGestureRecognizer, + // then we allow them to proceed iff full screen swipe is enabled. + if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { + return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? [topScreen isFullScreenSwipeBackEnabled] : NO; + } + if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer) { + return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? NO : [topScreen isFullScreenSwipeBackEnabled]; } } else { // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return topScreen.fullScreenSwipeEnabled; + return [topScreen isFullScreenSwipeBackEnabled]; } } #else // check for iOS >= 26 // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return topScreen.fullScreenSwipeEnabled; + return [topScreen isFullScreenSwipeBackEnabled]; } #endif // check for iOS >= 26 diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm index 89caf8d00d..1615e0dd75 100644 --- a/ios/RNSScreenStackAnimator.mm +++ b/ios/RNSScreenStackAnimator.mm @@ -88,7 +88,7 @@ - (void)animateTransition:(id)transitionCo if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] && ((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) { [self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController]; - } else if (screen.fullScreenSwipeEnabled && transitionContext.isInteractive) { + } else if ([screen isFullScreenSwipeBackEnabled] && transitionContext.isInteractive) { // we are swiping with full width gesture if (screen.customAnimationOnSwipe) { [self animateTransitionWithStackAnimation:screen.stackAnimation diff --git a/src/components/Screen.tsx b/src/components/Screen.tsx index bb28fe2ec1..9b84194c06 100644 --- a/src/components/Screen.tsx +++ b/src/components/Screen.tsx @@ -138,6 +138,7 @@ export const InnerScreen = React.forwardRef( activityState, children, isNativeStack, + fullScreenSwipeEnabled, gestureResponseDistance, scrollEdgeEffects, onGestureCancel, @@ -220,6 +221,13 @@ export const InnerScreen = React.forwardRef( sheetCornerRadius={sheetCornerRadius} sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge} sheetInitialDetent={resolvedSheetInitialDetentIndex} + fullScreenSwipeEnabled={ + fullScreenSwipeEnabled === undefined + ? 'undefined' + : fullScreenSwipeEnabled + ? 'true' + : 'false' + } gestureResponseDistance={{ start: gestureResponseDistance?.start ?? -1, end: gestureResponseDistance?.end ?? -1, diff --git a/src/fabric/ModalScreenNativeComponent.ts b/src/fabric/ModalScreenNativeComponent.ts index 167c4a6200..5675438eaa 100644 --- a/src/fabric/ModalScreenNativeComponent.ts +++ b/src/fabric/ModalScreenNativeComponent.ts @@ -67,6 +67,8 @@ type SwipeDirection = 'vertical' | 'horizontal'; type ReplaceAnimation = 'pop' | 'push'; +type OptionalBoolean = 'undefined' | 'false' | 'true'; + export interface NativeProps extends ViewProps { onAppear?: DirectEventHandler; onDisappear?: DirectEventHandler; @@ -88,7 +90,7 @@ export interface NativeProps extends ViewProps { sheetInitialDetent?: WithDefault; sheetElevation?: WithDefault; customAnimationOnSwipe?: boolean; - fullScreenSwipeEnabled?: boolean; + fullScreenSwipeEnabled?: WithDefault; fullScreenSwipeShadowEnabled?: WithDefault; homeIndicatorHidden?: boolean; preventNativeDismiss?: boolean; diff --git a/src/fabric/ScreenNativeComponent.ts b/src/fabric/ScreenNativeComponent.ts index aaa3a41ebf..e9844498af 100644 --- a/src/fabric/ScreenNativeComponent.ts +++ b/src/fabric/ScreenNativeComponent.ts @@ -69,6 +69,8 @@ type ReplaceAnimation = 'pop' | 'push'; type ScrollEdgeEffect = 'automatic' | 'hard' | 'soft' | 'hidden'; +type OptionalBoolean = 'undefined' | 'false' | 'true'; + export interface NativeProps extends ViewProps { onAppear?: DirectEventHandler; onDisappear?: DirectEventHandler; @@ -90,7 +92,7 @@ export interface NativeProps extends ViewProps { sheetInitialDetent?: WithDefault; sheetElevation?: WithDefault; customAnimationOnSwipe?: boolean; - fullScreenSwipeEnabled?: boolean; + fullScreenSwipeEnabled?: WithDefault; fullScreenSwipeShadowEnabled?: WithDefault; homeIndicatorHidden?: boolean; preventNativeDismiss?: boolean; From 67b5f79c7f41c24bed2d4dadf09b43b10c482afe Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Wed, 24 Sep 2025 08:40:34 +0200 Subject: [PATCH 2/9] Rename getter --- ios/RNSScreen.h | 6 +++++- ios/RNSScreen.mm | 2 +- ios/RNSScreenStack.mm | 8 ++++---- ios/RNSScreenStackAnimator.mm | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ios/RNSScreen.h b/ios/RNSScreen.h index 52c2c99693..065f22a341 100644 --- a/ios/RNSScreen.h +++ b/ios/RNSScreen.h @@ -164,7 +164,11 @@ namespace react = facebook::react; - (BOOL)isModal; - (BOOL)isPresentedAsNativeModal; -- (BOOL)isFullScreenSwipeBackEnabled; +/** + * Returns a boolean equivalen of fullScreenSwipeEnabled OptionalBoolean, resolves Undefined as `false` for iOS < 26, + * `true` otherwise. + */ +- (BOOL)fullScreenSwipeEnabledBoolean; /** * Tell `Screen` component that it has been removed from react state and can safely cleanup diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index 870e3dbc8d..d6a64cebff 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -891,7 +891,7 @@ - (BOOL)isPresentedAsNativeModal return self.controller.parentViewController == nil && self.controller.presentingViewController != nil; } -- (BOOL)isFullScreenSwipeBackEnabled +- (BOOL)fullScreenSwipeEnabledBoolean { switch (self.fullScreenSwipeEnabled) { case RNSOptionalBooleanTrue: diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index 5adbe88efd..c2cad2b4dc 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -1202,21 +1202,21 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive // either interactiveContentPopGestureRecognizer or RNSPanGestureRecognizer, // then we allow them to proceed iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? [topScreen isFullScreenSwipeBackEnabled] : NO; + return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? [topScreen fullScreenSwipeEnabledBoolean] : NO; } if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer) { - return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? NO : [topScreen isFullScreenSwipeBackEnabled]; + return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? NO : [topScreen fullScreenSwipeEnabledBoolean]; } } else { // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return [topScreen isFullScreenSwipeBackEnabled]; + return [topScreen fullScreenSwipeEnabledBoolean]; } } #else // check for iOS >= 26 // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return [topScreen isFullScreenSwipeBackEnabled]; + return [topScreen fullScreenSwipeEnabledBoolean]; } #endif // check for iOS >= 26 diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm index 1615e0dd75..17ad8aec1f 100644 --- a/ios/RNSScreenStackAnimator.mm +++ b/ios/RNSScreenStackAnimator.mm @@ -88,7 +88,7 @@ - (void)animateTransition:(id)transitionCo if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] && ((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) { [self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController]; - } else if ([screen isFullScreenSwipeBackEnabled] && transitionContext.isInteractive) { + } else if ([screen fullScreenSwipeEnabledBoolean] && transitionContext.isInteractive) { // we are swiping with full width gesture if (screen.customAnimationOnSwipe) { [self animateTransitionWithStackAnimation:screen.stackAnimation From 58f76c0b3aa9556b1fed74514889161b3ef45a6e Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Wed, 24 Sep 2025 09:16:44 +0200 Subject: [PATCH 3/9] Fix android stub implementation --- .../src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt index a96ebe83e4..7bb6d7c3ed 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenViewManager.kt @@ -273,7 +273,7 @@ open class ScreenViewManager : // these props are not available on Android, however we must override their setters override fun setFullScreenSwipeEnabled( view: Screen?, - value: Boolean, + value: String?, ) = Unit override fun setFullScreenSwipeShadowEnabled( From ac854cd7b970a5f91e55a5c618c5decc53160a3a Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 25 Sep 2025 10:02:48 +0200 Subject: [PATCH 4/9] Update docs for fullScreenSwipeEnabled --- guides/GUIDE_FOR_LIBRARY_AUTHORS.md | 12 ++++++++++-- src/types.tsx | 13 +++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md index b2b5ffee69..6b493d6907 100644 --- a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md +++ b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md @@ -54,8 +54,16 @@ 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. +Boolean indicating whether the swipe gesture should work on whole screen. The behavior depends on iOS version. + +For iOS prior to 26, 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. + +For iOS 26 and up, native `interactiveContentPopGestureRecognizer` is used, and this prop controls whether it should +be enabled or not. + +When not set, it defaults to `false` on iOS < 26 and `true` for iOS >= 26. ### `fullScreenSwipeShadowEnabled` (iOS only) diff --git a/src/types.tsx b/src/types.tsx index a91e2ad130..3c75884dc3 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -139,11 +139,16 @@ export interface ScreenProps extends ViewProps { */ freezeOnBlur?: boolean; /** - * 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`. + * Boolean indicating whether the swipe gesture should work on whole screen. The behavior depends on iOS version. + * + * For iOS prior to 26, 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. + * + * For iOS 26 and up, native `interactiveContentPopGestureRecognizer` is used, and this prop controls whether it should + * be enabled or not. * - * @deprecated since iOS 26, full screen swipe is handled by native recognizer, and this prop is ignored. + * When not set, it defaults to `false` on iOS < 26 and `true` for iOS >= 26. * * @platform ios */ From 952fbb14f017252ffe2528c64fbb826590b8fb1e Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 25 Sep 2025 12:10:07 +0200 Subject: [PATCH 5/9] Fix typo, add prop initialization --- ios/RNSScreen.h | 2 +- ios/RNSScreen.mm | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ios/RNSScreen.h b/ios/RNSScreen.h index 065f22a341..b098b37ea2 100644 --- a/ios/RNSScreen.h +++ b/ios/RNSScreen.h @@ -165,7 +165,7 @@ namespace react = facebook::react; - (BOOL)isPresentedAsNativeModal; /** - * Returns a boolean equivalen of fullScreenSwipeEnabled OptionalBoolean, resolves Undefined as `false` for iOS < 26, + * Returns a boolean equivalent of fullScreenSwipeEnabled OptionalBoolean, resolves Undefined as `false` for iOS < 26, * `true` otherwise. */ - (BOOL)fullScreenSwipeEnabledBoolean; diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index d6a64cebff..f68551ce62 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -131,6 +131,7 @@ - (void)initCommonProps _hasOrientationSet = NO; _hasHomeIndicatorHiddenSet = NO; _activityState = RNSActivityStateUndefined; + _fullScreenSwipeEnabled = RNSOptionalBooleanUndefined; _fullScreenSwipeShadowEnabled = YES; _shouldUpdateScrollEdgeEffects = NO; #if !TARGET_OS_TV From 125c413c9541f96171b68d1a0ab537d002c36867 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Tue, 30 Sep 2025 07:57:39 +0200 Subject: [PATCH 6/9] Rephrase docs --- guides/GUIDE_FOR_LIBRARY_AUTHORS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md index 6b493d6907..9b5264f640 100644 --- a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md +++ b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md @@ -60,8 +60,8 @@ For iOS prior to 26, swiping with this option results in the same transition ani 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. -For iOS 26 and up, native `interactiveContentPopGestureRecognizer` is used, and this prop controls whether it should -be enabled or not. +For iOS 26 and up, native `interactiveContentPopGestureRecognizer` is used for all cases except for `customAnimationOnSwipe`, +however the prop still can be used to controls whether it should be enabled or not. When not set, it defaults to `false` on iOS < 26 and `true` for iOS >= 26. From 6ea670cd52b0c443a67aeb3b61032317b6644a46 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Tue, 30 Sep 2025 08:07:24 +0200 Subject: [PATCH 7/9] Use parseBooleanToOptionalBooleanNativeProp --- src/components/Screen.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/components/Screen.tsx b/src/components/Screen.tsx index 9b84194c06..edad88aae3 100644 --- a/src/components/Screen.tsx +++ b/src/components/Screen.tsx @@ -29,6 +29,7 @@ import { resolveSheetInitialDetentIndex, resolveSheetLargestUndimmedDetent, } from './helpers/sheet'; +import { parseBooleanToOptionalBooleanNativeProp } from '../utils'; type NativeProps = ScreenNativeComponentProps | ModalScreenNativeComponentProps; const AnimatedNativeScreen = Animated.createAnimatedComponent( @@ -221,13 +222,9 @@ export const InnerScreen = React.forwardRef( sheetCornerRadius={sheetCornerRadius} sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge} sheetInitialDetent={resolvedSheetInitialDetentIndex} - fullScreenSwipeEnabled={ - fullScreenSwipeEnabled === undefined - ? 'undefined' - : fullScreenSwipeEnabled - ? 'true' - : 'false' - } + fullScreenSwipeEnabled={parseBooleanToOptionalBooleanNativeProp( + fullScreenSwipeEnabled, + )} gestureResponseDistance={{ start: gestureResponseDistance?.start ?? -1, end: gestureResponseDistance?.end ?? -1, From 62e1a05aa6d86f53e9aadc7318581b18ee7f9189 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Wed, 1 Oct 2025 14:33:35 +0200 Subject: [PATCH 8/9] Refactor fullScreenSwipeEnabled property --- ios/RNSScreen.h | 8 +------- ios/RNSScreen.mm | 14 ++++++++++---- ios/RNSScreenStack.mm | 8 ++++---- ios/RNSScreenStackAnimator.mm | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ios/RNSScreen.h b/ios/RNSScreen.h index b098b37ea2..fcc20cfb28 100644 --- a/ios/RNSScreen.h +++ b/ios/RNSScreen.h @@ -72,7 +72,7 @@ namespace react = facebook::react; RNSSafeAreaProviding, RNSScrollEdgeEffectProviding> -@property (nonatomic) RNSOptionalBoolean fullScreenSwipeEnabled; +@property (nonatomic, readonly) BOOL fullScreenSwipeEnabled; @property (nonatomic) BOOL fullScreenSwipeShadowEnabled; @property (nonatomic) BOOL gestureEnabled; @property (nonatomic) BOOL hasStatusBarHiddenSet; @@ -164,12 +164,6 @@ namespace react = facebook::react; - (BOOL)isModal; - (BOOL)isPresentedAsNativeModal; -/** - * Returns a boolean equivalent of fullScreenSwipeEnabled OptionalBoolean, resolves Undefined as `false` for iOS < 26, - * `true` otherwise. - */ -- (BOOL)fullScreenSwipeEnabledBoolean; - /** * Tell `Screen` component that it has been removed from react state and can safely cleanup * any retained resources. diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index f68551ce62..85046f3a7b 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -74,6 +74,7 @@ @implementation RNSScreenView { ContentWrapperBox _contentWrapperBox; bool _sheetHasInitialDetentSet; BOOL _shouldUpdateScrollEdgeEffects; + RNSOptionalBoolean _fullScreenSwipeEnabled; #ifdef RCT_NEW_ARCH_ENABLED RCTSurfaceTouchHandler *_touchHandler; react::RNSScreenShadowNode::ConcreteState::Shared _state; @@ -892,9 +893,9 @@ - (BOOL)isPresentedAsNativeModal return self.controller.parentViewController == nil && self.controller.presentingViewController != nil; } -- (BOOL)fullScreenSwipeEnabledBoolean +- (BOOL)fullScreenSwipeEnabled { - switch (self.fullScreenSwipeEnabled) { + switch (_fullScreenSwipeEnabled) { case RNSOptionalBooleanTrue: return YES; case RNSOptionalBooleanFalse: @@ -1349,8 +1350,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: const auto &oldScreenProps = *std::static_pointer_cast(_props); const auto &newScreenProps = *std::static_pointer_cast(props); - [self setFullScreenSwipeEnabled:[RNSConvert RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent: - newScreenProps.fullScreenSwipeEnabled]]; + _fullScreenSwipeEnabled = + [RNSConvert RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent:newScreenProps.fullScreenSwipeEnabled]; [self setFullScreenSwipeShadowEnabled:newScreenProps.fullScreenSwipeShadowEnabled]; @@ -1542,6 +1543,11 @@ - (void)reactSetFrame:(CGRect)frame // subviews } +- (void)setFullScreenSwipeEnabled:(RNSOptionalBoolean)fullScreenSwipeEnabled +{ + _fullScreenSwipeEnabled = fullScreenSwipeEnabled; +} + #endif @end diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index c2cad2b4dc..b2d840d743 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -1202,21 +1202,21 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive // either interactiveContentPopGestureRecognizer or RNSPanGestureRecognizer, // then we allow them to proceed iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? [topScreen fullScreenSwipeEnabledBoolean] : NO; + return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? [topScreen fullScreenSwipeEnabled] : NO; } if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer) { - return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? NO : [topScreen fullScreenSwipeEnabledBoolean]; + return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? NO : [topScreen fullScreenSwipeEnabled]; } } else { // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return [topScreen fullScreenSwipeEnabledBoolean]; + return [topScreen fullScreenSwipeEnabled]; } } #else // check for iOS >= 26 // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return [topScreen fullScreenSwipeEnabledBoolean]; + return [topScreen fullScreenSwipeEnabled]; } #endif // check for iOS >= 26 diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm index 17ad8aec1f..8dc0962b59 100644 --- a/ios/RNSScreenStackAnimator.mm +++ b/ios/RNSScreenStackAnimator.mm @@ -88,7 +88,7 @@ - (void)animateTransition:(id)transitionCo if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] && ((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) { [self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController]; - } else if ([screen fullScreenSwipeEnabledBoolean] && transitionContext.isInteractive) { + } else if ([screen fullScreenSwipeEnabled] && transitionContext.isInteractive) { // we are swiping with full width gesture if (screen.customAnimationOnSwipe) { [self animateTransitionWithStackAnimation:screen.stackAnimation From ffc399549291529941b93f1df81ef1ee48ab9d73 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Thu, 2 Oct 2025 08:46:10 +0200 Subject: [PATCH 9/9] Introduce derived property `isFullScreenSwipeEffectivelyEnabled` --- ios/RNSScreen.h | 8 +++++++- ios/RNSScreen.mm | 36 +++++++++++++++-------------------- ios/RNSScreenStack.mm | 10 ++++++---- ios/RNSScreenStackAnimator.mm | 2 +- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/ios/RNSScreen.h b/ios/RNSScreen.h index fcc20cfb28..6b0c87abb7 100644 --- a/ios/RNSScreen.h +++ b/ios/RNSScreen.h @@ -72,7 +72,13 @@ namespace react = facebook::react; RNSSafeAreaProviding, RNSScrollEdgeEffectProviding> -@property (nonatomic, readonly) BOOL fullScreenSwipeEnabled; +/** + * This is value of the prop as passed by the user. To get effective value see derived property + * `isFullScreenSwipeEffectivelyEnabled` + */ +@property (nonatomic) RNSOptionalBoolean fullScreenSwipeEnabled; +@property (nonatomic, readonly, getter=isFullScreenSwipeEffectivelyEnabled) BOOL fullScreenSwipeEffectivelyEnabled; + @property (nonatomic) BOOL fullScreenSwipeShadowEnabled; @property (nonatomic) BOOL gestureEnabled; @property (nonatomic) BOOL hasStatusBarHiddenSet; diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index 85046f3a7b..0ab083c926 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -74,7 +74,6 @@ @implementation RNSScreenView { ContentWrapperBox _contentWrapperBox; bool _sheetHasInitialDetentSet; BOOL _shouldUpdateScrollEdgeEffects; - RNSOptionalBoolean _fullScreenSwipeEnabled; #ifdef RCT_NEW_ARCH_ENABLED RCTSurfaceTouchHandler *_touchHandler; react::RNSScreenShadowNode::ConcreteState::Shared _state; @@ -469,6 +468,21 @@ - (void)setTopScrollEdgeEffect:(RNSScrollEdgeEffect)topScrollEdgeEffect _topScrollEdgeEffect = topScrollEdgeEffect; } +- (BOOL)isFullScreenSwipeEffectivelyEnabled +{ + switch (_fullScreenSwipeEnabled) { + case RNSOptionalBooleanTrue: + return YES; + case RNSOptionalBooleanFalse: + return NO; + case RNSOptionalBooleanUndefined: + if (@available(iOS 26, *)) { + return YES; + } + return NO; + } +} + RNS_IGNORE_SUPER_CALL_BEGIN - (UIView *)reactSuperview { @@ -893,21 +907,6 @@ - (BOOL)isPresentedAsNativeModal return self.controller.parentViewController == nil && self.controller.presentingViewController != nil; } -- (BOOL)fullScreenSwipeEnabled -{ - switch (_fullScreenSwipeEnabled) { - case RNSOptionalBooleanTrue: - return YES; - case RNSOptionalBooleanFalse: - return NO; - case RNSOptionalBooleanUndefined: - if (@available(iOS 26, *)) { - return YES; - } - return NO; - } -} - - (BOOL)isFullscreenModal { switch (self.controller.modalPresentationStyle) { @@ -1543,11 +1542,6 @@ - (void)reactSetFrame:(CGRect)frame // subviews } -- (void)setFullScreenSwipeEnabled:(RNSOptionalBoolean)fullScreenSwipeEnabled -{ - _fullScreenSwipeEnabled = fullScreenSwipeEnabled; -} - #endif @end diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index b2d840d743..1b69135c17 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -1202,21 +1202,23 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceive // either interactiveContentPopGestureRecognizer or RNSPanGestureRecognizer, // then we allow them to proceed iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? [topScreen fullScreenSwipeEnabled] : NO; + return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? topScreen.isFullScreenSwipeEffectivelyEnabled + : NO; } if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer) { - return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? NO : [topScreen fullScreenSwipeEnabled]; + return customAnimationOnSwipePropSetAndSelectedAnimationIsCustom ? NO + : topScreen.isFullScreenSwipeEffectivelyEnabled; } } else { // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return [topScreen fullScreenSwipeEnabled]; + return topScreen.isFullScreenSwipeEffectivelyEnabled; } } #else // check for iOS >= 26 // We want to pass events to RNSPanGestureRecognizer iff full screen swipe is enabled. if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) { - return [topScreen fullScreenSwipeEnabled]; + return topScreen.isFullScreenSwipeEffectivelyEnabled; } #endif // check for iOS >= 26 diff --git a/ios/RNSScreenStackAnimator.mm b/ios/RNSScreenStackAnimator.mm index 8dc0962b59..2d27a87af0 100644 --- a/ios/RNSScreenStackAnimator.mm +++ b/ios/RNSScreenStackAnimator.mm @@ -88,7 +88,7 @@ - (void)animateTransition:(id)transitionCo if ([screen.reactSuperview isKindOfClass:[RNSScreenStackView class]] && ((RNSScreenStackView *)(screen.reactSuperview)).customAnimation) { [self animateWithNoAnimation:transitionContext toVC:toViewController fromVC:fromViewController]; - } else if ([screen fullScreenSwipeEnabled] && transitionContext.isInteractive) { + } else if (screen.isFullScreenSwipeEffectivelyEnabled && transitionContext.isInteractive) { // we are swiping with full width gesture if (screen.customAnimationOnSwipe) { [self animateTransitionWithStackAnimation:screen.stackAnimation