Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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, and this prop controls whether it should
be enabled or not.
Copy link
Member

Choose a reason for hiding this comment

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

I get impression from this description that customAnimationOnSwipe does not work anymore on iOS 26 & I remember from testing that was not the case, was it?

If it still works, we should rephrase the description so that it indicates that customAnimationOnSwipe is possible in both cases but it results in new native gesture recognizer not being used.

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 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
8 changes: 7 additions & 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) RNSOptionalBoolean fullScreenSwipeEnabled;
@property (nonatomic) BOOL fullScreenSwipeShadowEnabled;
@property (nonatomic) BOOL gestureEnabled;
@property (nonatomic) BOOL hasStatusBarHiddenSet;
Expand Down Expand Up @@ -164,6 +164,12 @@ namespace react = facebook::react;
- (BOOL)isModal;
- (BOOL)isPresentedAsNativeModal;

/**
* 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
* any retained resources.
Expand Down
20 changes: 18 additions & 2 deletions ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,21 @@ - (BOOL)isPresentedAsNativeModal
return self.controller.parentViewController == nil && self.controller.presentingViewController != nil;
}

- (BOOL)fullScreenSwipeEnabledBoolean
{
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) {
Expand Down Expand Up @@ -1333,7 +1348,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];
[self setFullScreenSwipeEnabled:[RNSConvert RNSOptionalBooleanFromRNSFullScreenSwipeEnabledCppEquivalent:
newScreenProps.fullScreenSwipeEnabled]];

[self setFullScreenSwipeShadowEnabled:newScreenProps.fullScreenSwipeShadowEnabled];

Expand Down Expand Up @@ -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)
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 fullScreenSwipeEnabledBoolean] : NO;
}
if (gestureRecognizer == _controller.interactiveContentPopGestureRecognizer) {
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.fullScreenSwipeEnabled;
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.fullScreenSwipeEnabled;
return [topScreen fullScreenSwipeEnabledBoolean];
}
#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 fullScreenSwipeEnabledBoolean] && transitionContext.isInteractive) {
// we are swiping with full width gesture
if (screen.customAnimationOnSwipe) {
[self animateTransitionWithStackAnimation:screen.stackAnimation
Expand Down
8 changes: 8 additions & 0 deletions src/components/Screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
React.useImperativeHandle(ref, () => innerRef.current!, []);
const prevActivityState = usePrevious(props.activityState);

const setRef = (ref: ViewConfig) => {

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
innerRef.current = ref;
props.onComponentRef?.(ref);
};
Expand Down Expand Up @@ -138,11 +138,12 @@
activityState,
children,
isNativeStack,
fullScreenSwipeEnabled,
gestureResponseDistance,
scrollEdgeEffects,
onGestureCancel,
style,
...props

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
} = rest;

if (active !== undefined && activityState === undefined) {
Expand All @@ -164,7 +165,7 @@
}
}

const handleRef = (ref: ViewConfig) => {

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
// Workaround is necessary to prevent React Native from hiding frozen screens.
// See this PR: https://github.com/grahammendick/navigation/pull/860
if (ref?.viewConfig?.validAttributes?.style) {
Expand Down Expand Up @@ -220,6 +221,13 @@
sheetCornerRadius={sheetCornerRadius}
sheetExpandsWhenScrolledToEdge={sheetExpandsWhenScrolledToEdge}
sheetInitialDetent={resolvedSheetInitialDetentIndex}
fullScreenSwipeEnabled={
fullScreenSwipeEnabled === undefined
? 'undefined'
: fullScreenSwipeEnabled
? 'true'
: 'false'
}
gestureResponseDistance={{
start: gestureResponseDistance?.start ?? -1,
end: gestureResponseDistance?.end ?? -1,
Expand Down Expand Up @@ -272,7 +280,7 @@
style,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onComponentRef,
...props

Check warning on line 283 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
} = rest;

if (active !== undefined && activityState === undefined) {
Expand All @@ -280,7 +288,7 @@
}
return (
<Animated.View
style={[style, { display: activityState !== 0 ? 'flex' : 'none' }]}

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

View workflow job for this annotation

GitHub Actions / lint-js

Inline 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