Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public interface RNSScreenManagerInterface<T extends View> {
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);
Expand Down
12 changes: 10 additions & 2 deletions guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

### `fullScreenSwipeShadowEnabled` (iOS only)

Expand Down
3 changes: 3 additions & 0 deletions ios/RNSConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ namespace react = facebook::react;
+ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent:
(react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode;

+ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent:
(react::RNSScreenFullScreenSwipeEnabled)fullScreenSwipeEnabled;

+ (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent:
(react::RNSScreenStackPresentation)stackPresentation;

Expand Down
14 changes: 14 additions & 0 deletions ios/RNSConvert.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion ios/RNSScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ namespace react = facebook::react;
RNSSafeAreaProviding,
RNSScrollEdgeEffectProviding>

@property (nonatomic) BOOL fullScreenSwipeEnabled;
@property (nonatomic, readonly) BOOL fullScreenSwipeEnabled;
@property (nonatomic) BOOL fullScreenSwipeShadowEnabled;
@property (nonatomic) BOOL gestureEnabled;
@property (nonatomic) BOOL hasStatusBarHiddenSet;
Expand Down
27 changes: 25 additions & 2 deletions ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -131,6 +132,7 @@ - (void)initCommonProps
_hasOrientationSet = NO;
_hasHomeIndicatorHiddenSet = NO;
_activityState = RNSActivityStateUndefined;
_fullScreenSwipeEnabled = RNSOptionalBooleanUndefined;
_fullScreenSwipeShadowEnabled = YES;
_shouldUpdateScrollEdgeEffects = NO;
#if !TARGET_OS_TV
Expand Down Expand Up @@ -891,6 +893,21 @@ - (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) {
Expand Down Expand Up @@ -1333,7 +1350,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::
const auto &oldScreenProps = *std::static_pointer_cast<const react::RNSScreenProps>(_props);
const auto &newScreenProps = *std::static_pointer_cast<const react::RNSScreenProps>(props);

[self setFullScreenSwipeEnabled:newScreenProps.fullScreenSwipeEnabled];
_fullScreenSwipeEnabled =
[RNSConvert RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent:newScreenProps.fullScreenSwipeEnabled];

[self setFullScreenSwipeShadowEnabled:newScreenProps.fullScreenSwipeShadowEnabled];

Expand Down Expand Up @@ -1525,6 +1543,11 @@ - (void)reactSetFrame:(CGRect)frame
// subviews
}

- (void)setFullScreenSwipeEnabled:(RNSOptionalBoolean)fullScreenSwipeEnabled
{
_fullScreenSwipeEnabled = fullScreenSwipeEnabled;
}

#endif

@end
Expand Down Expand Up @@ -2171,7 +2194,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)
Expand Down
19 changes: 10 additions & 9 deletions ios/RNSScreenStack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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 fullScreenSwipeEnabled] : NO;
}
if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer) {
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.fullScreenSwipeEnabled;
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.fullScreenSwipeEnabled;
return [topScreen fullScreenSwipeEnabled];
}
#endif // check for iOS >= 26

Expand Down
2 changes: 1 addition & 1 deletion ios/RNSScreenStackAnimator.mm
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ - (void)animateTransition:(id<UIViewControllerContextTransitioning>)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 fullScreenSwipeEnabled] && transitionContext.isInteractive) {
// we are swiping with full width gesture
if (screen.customAnimationOnSwipe) {
[self animateTransitionWithStackAnimation:screen.stackAnimation
Expand Down
5 changes: 5 additions & 0 deletions src/components/Screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
resolveSheetInitialDetentIndex,
resolveSheetLargestUndimmedDetent,
} from './helpers/sheet';
import { parseBooleanToOptionalBooleanNativeProp } from '../utils';

type NativeProps = ScreenNativeComponentProps | ModalScreenNativeComponentProps;
const AnimatedNativeScreen = Animated.createAnimatedComponent(
Expand Down Expand Up @@ -62,7 +63,7 @@
const innerRef = React.useRef<ViewConfig | null>(null);
React.useImperativeHandle(ref, () => innerRef.current!, []);
const prevActivityState = usePrevious(props.activityState);

Check warning on line 66 in src/components/Screen.tsx

View workflow job for this annotation

GitHub Actions / lint-js

'ref' is already declared in the upper scope on line 61 column 31
const setRef = (ref: ViewConfig) => {
innerRef.current = ref;
props.onComponentRef?.(ref);
Expand Down Expand Up @@ -138,10 +139,11 @@
activityState,
children,
isNativeStack,
fullScreenSwipeEnabled,
gestureResponseDistance,
scrollEdgeEffects,
onGestureCancel,
style,

Check warning on line 146 in src/components/Screen.tsx

View workflow job for this annotation

GitHub Actions / lint-js

'props' is already declared in the upper scope on line 61 column 24
...props
} = rest;

Expand All @@ -163,7 +165,7 @@
);
}
}

Check warning on line 168 in src/components/Screen.tsx

View workflow job for this annotation

GitHub Actions / lint-js

'ref' is already declared in the upper scope on line 61 column 31
const handleRef = (ref: ViewConfig) => {
// Workaround is necessary to prevent React Native from hiding frozen screens.
// See this PR: https://github.com/grahammendick/navigation/pull/860
Expand Down Expand Up @@ -220,6 +222,9 @@
sheetCornerRadius={sheetCornerRadius}
sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge}
sheetInitialDetent={resolvedSheetInitialDetentIndex}
fullScreenSwipeEnabled={parseBooleanToOptionalBooleanNativeProp(
fullScreenSwipeEnabled,
)}
gestureResponseDistance={{
start: gestureResponseDistance?.start ?? -1,
end: gestureResponseDistance?.end ?? -1,
Expand Down Expand Up @@ -271,7 +276,7 @@
activityState,
style,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onComponentRef,

Check warning on line 279 in src/components/Screen.tsx

View workflow job for this annotation

GitHub Actions / lint-js

'props' is already declared in the upper scope on line 61 column 24
...props
} = rest;

Expand All @@ -279,7 +284,7 @@
activityState = active !== 0 ? 2 : 0;
}
return (
<Animated.View

Check warning on line 287 in src/components/Screen.tsx

View workflow job for this annotation

GitHub Actions / lint-js

Inline style: { display: "activityState !== 0 ? 'flex' : 'none'" }
style={[style, { display: activityState !== 0 ? 'flex' : 'none' }]}
ref={setRef}
{...props}
Expand Down
4 changes: 3 additions & 1 deletion src/fabric/ModalScreenNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ type SwipeDirection = 'vertical' | 'horizontal';

type ReplaceAnimation = 'pop' | 'push';

type OptionalBoolean = 'undefined' | 'false' | 'true';

export interface NativeProps extends ViewProps {
onAppear?: DirectEventHandler<ScreenEvent>;
onDisappear?: DirectEventHandler<ScreenEvent>;
Expand All @@ -88,7 +90,7 @@ export interface NativeProps extends ViewProps {
sheetInitialDetent?: WithDefault<Int32, 0>;
sheetElevation?: WithDefault<Int32, 24>;
customAnimationOnSwipe?: boolean;
fullScreenSwipeEnabled?: boolean;
fullScreenSwipeEnabled?: WithDefault<OptionalBoolean, 'undefined'>;
fullScreenSwipeShadowEnabled?: WithDefault<boolean, true>;
homeIndicatorHidden?: boolean;
preventNativeDismiss?: boolean;
Expand Down
4 changes: 3 additions & 1 deletion src/fabric/ScreenNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ScreenEvent>;
onDisappear?: DirectEventHandler<ScreenEvent>;
Expand All @@ -90,7 +92,7 @@ export interface NativeProps extends ViewProps {
sheetInitialDetent?: WithDefault<Int32, 0>;
sheetElevation?: WithDefault<Int32, 24>;
customAnimationOnSwipe?: boolean;
fullScreenSwipeEnabled?: boolean;
fullScreenSwipeEnabled?: WithDefault<OptionalBoolean, 'undefined'>;
fullScreenSwipeShadowEnabled?: WithDefault<boolean, true>;
homeIndicatorHidden?: boolean;
preventNativeDismiss?: boolean;
Expand Down
13 changes: 9 additions & 4 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Loading