Skip to content

Commit

Permalink
feat(iOS): add fullScreenSwipeShadowEnabled prop for controlling sh…
Browse files Browse the repository at this point in the history
…adow during swipe gesture (software-mansion#2239)

## Description

When using full screen swipe back there was no shadow under the view.
Related PR software-mansion#2234 with discussion about this change.

Corresponding PR in `react-navigation`:

* react-navigation/react-navigation#12053

## Changes

Added shadow to transition in
animateSimplePushWithTransitionContext:toVC:fromVC:.

## Test code and steps to reproduce

1. Run `Test2227` in TestsExample app.
2. Push screen with Go to Details.
3. Swipe back with full screen swipe gesture.
5. Before this fix there would be no shadow during transition with full
screen swipe back.

## Checklist

- [x] Included code example that can be used to test this change
- [ ] Updated TS types
- [x] Updated documentation: <!-- For adding new props to native-stack
-->
- [x]
https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md
- [x]
https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md
- [x]
https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx
- [x]
https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx
- [ ] Ensured that CI passes
  • Loading branch information
maksg authored and ja1ns committed Oct 9, 2024
1 parent dfeacf1 commit 776561a
Show file tree
Hide file tree
Showing 15 changed files with 94 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ open class ScreenViewManager :
value: Boolean,
) = Unit

override fun setFullScreenSwipeShadowEnabled(
view: Screen?,
value: Boolean,
) = Unit

override fun setTransitionDuration(
view: Screen?,
value: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "fullScreenSwipeEnabled":
mViewManager.setFullScreenSwipeEnabled(view, value == null ? false : (boolean) value);
break;
case "fullScreenSwipeShadowEnabled":
mViewManager.setFullScreenSwipeShadowEnabled(view, value == null ? false : (boolean) value);
break;
case "homeIndicatorHidden":
mViewManager.setHomeIndicatorHidden(view, value == null ? false : (boolean) value);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public interface RNSScreenManagerInterface<T extends View> {
void setSheetExpandsWhenScrolledToEdge(T view, boolean value);
void setCustomAnimationOnSwipe(T view, boolean value);
void setFullScreenSwipeEnabled(T view, boolean value);
void setFullScreenSwipeShadowEnabled(T view, boolean value);
void setHomeIndicatorHidden(T view, boolean value);
void setPreventNativeDismiss(T view, boolean value);
void setGestureEnabled(T view, boolean value);
Expand Down
1 change: 1 addition & 0 deletions apps/src/tests/Test2227.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export default function App() {
<Stack.Navigator
screenOptions={{
fullScreenGestureEnabled: true,
fullScreenGestureShadowEnabled: true,
}}>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
Expand Down
6 changes: 6 additions & 0 deletions guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ Defaults to `false`. When `enableFreeze()` is run at the top of the application

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`.

### `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 `false`.

### `gestureEnabled` (iOS only)

When set to `false` the back swipe gesture will be disabled. The default value is `true`.
Expand Down
1 change: 1 addition & 0 deletions ios/RNSScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ namespace react = facebook::react;
#endif

@property (nonatomic) BOOL fullScreenSwipeEnabled;
@property (nonatomic) BOOL fullScreenSwipeShadowEnabled;
@property (nonatomic) BOOL gestureEnabled;
@property (nonatomic) BOOL hasStatusBarHiddenSet;
@property (nonatomic) BOOL hasStatusBarStyleSet;
Expand Down
3 changes: 3 additions & 0 deletions ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::

[self setFullScreenSwipeEnabled:newScreenProps.fullScreenSwipeEnabled];

[self setFullScreenSwipeShadowEnabled:newScreenProps.fullScreenSwipeShadowEnabled];

[self setGestureEnabled:newScreenProps.gestureEnabled];

[self setTransitionDuration:[NSNumber numberWithInt:newScreenProps.transitionDuration]];
Expand Down Expand Up @@ -1427,6 +1429,7 @@ @implementation RNSScreenManager
RCT_REMAP_VIEW_PROPERTY(activityState, activityStateOrNil, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(customAnimationOnSwipe, BOOL);
RCT_EXPORT_VIEW_PROPERTY(fullScreenSwipeEnabled, BOOL);
RCT_EXPORT_VIEW_PROPERTY(fullScreenSwipeShadowEnabled, BOOL);
RCT_EXPORT_VIEW_PROPERTY(gestureEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(gestureResponseDistance, NSDictionary)
RCT_EXPORT_VIEW_PROPERTY(hideKeyboardOnSwipe, BOOL)
Expand Down
49 changes: 43 additions & 6 deletions ios/RNSScreenStackAnimator.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
static const float RNSSlideCloseTransitionDurationProportion = 0.25 / 0.35;
static const float RNSFadeCloseTransitionDurationProportion = 0.15 / 0.35;
static const float RNSFadeCloseDelayTransitionDurationProportion = 0.1 / 0.35;
// same value is used in other projects using similar approach for transistions
// and it looks the most similar to the value used by Apple
static constexpr float RNSShadowViewMaxAlpha = 0.1;

@implementation RNSScreenStackAnimator {
UINavigationControllerOperation _operation;
Expand Down Expand Up @@ -71,27 +74,33 @@ - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionCo
// we are swiping with full width gesture
if (screen.customAnimationOnSwipe) {
[self animateTransitionWithStackAnimation:screen.stackAnimation
shadowEnabled:screen.fullScreenSwipeShadowEnabled
transitionContext:transitionContext
toVC:toViewController
fromVC:fromViewController];
} else {
// we have to provide an animation when swiping, otherwise the screen will be popped immediately,
// so in case of no custom animation on swipe set, we provide the one closest to the default
[self animateSimplePushWithTransitionContext:transitionContext toVC:toViewController fromVC:fromViewController];
[self animateSimplePushWithShadowEnabled:screen.fullScreenSwipeShadowEnabled
transitionContext:transitionContext
toVC:toViewController
fromVC:fromViewController];
}
} else {
// we are going forward or provided custom animation on swipe or clicked native header back button
[self animateTransitionWithStackAnimation:screen.stackAnimation
shadowEnabled:screen.fullScreenSwipeShadowEnabled
transitionContext:transitionContext
toVC:toViewController
fromVC:fromViewController];
}
}
}

- (void)animateSimplePushWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
- (void)animateSimplePushWithShadowEnabled:(BOOL)shadowEnabled
transitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
float containerWidth = transitionContext.containerView.bounds.size.width;
float belowViewWidth = containerWidth * 0.3;
Expand All @@ -105,28 +114,55 @@ - (void)animateSimplePushWithTransitionContext:(id<UIViewControllerContextTransi
leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0);
}

UIView *shadowView;
if (shadowEnabled) {
shadowView = [[UIView alloc] initWithFrame:fromViewController.view.frame];
shadowView.backgroundColor = [UIColor blackColor];
}

if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = rightTransform;
[[transitionContext containerView] addSubview:toViewController.view];
if (shadowView) {
[[transitionContext containerView] insertSubview:shadowView belowSubview:toViewController.view];
shadowView.alpha = 0.0;
}

[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
fromViewController.view.transform = leftTransform;
toViewController.view.transform = CGAffineTransformIdentity;
if (shadowView) {
shadowView.alpha = RNSShadowViewMaxAlpha;
}
}
completion:^(BOOL finished) {
if (shadowView) {
[shadowView removeFromSuperview];
}
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = leftTransform;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
if (shadowView) {
[[transitionContext containerView] insertSubview:shadowView belowSubview:fromViewController.view];
shadowView.alpha = RNSShadowViewMaxAlpha;
}

void (^animationBlock)(void) = ^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = rightTransform;
if (shadowView) {
shadowView.alpha = 0.0;
}
};
void (^completionBlock)(BOOL) = ^(BOOL finished) {
if (shadowView) {
[shadowView removeFromSuperview];
}
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
Expand Down Expand Up @@ -381,12 +417,13 @@ + (BOOL)isCustomAnimation:(RNSScreenStackAnimation)animation
}

- (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation
shadowEnabled:(BOOL)shadowEnabled
transitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toVC
fromVC:(UIViewController *)fromVC
{
if (animation == RNSScreenStackAnimationSimplePush) {
[self animateSimplePushWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
[self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
} else if (animation == RNSScreenStackAnimationSlideFromLeft) {
[self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
Expand All @@ -402,7 +439,7 @@ - (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation
return;
}
// simple_push is the default custom animation
[self animateSimplePushWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
[self animateSimplePushWithShadowEnabled:shadowEnabled transitionContext:transitionContext toVC:toVC fromVC:fromVC];
}

@end
6 changes: 6 additions & 0 deletions native-stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ Enum value indicating display mode of **default** back button. It works on iOS >

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`.

### `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 `false`.

#### `gestureEnabled` (iOS only)

Whether you can use gestures to dismiss this screen. Defaults to `true`.
Expand Down
2 changes: 1 addition & 1 deletion react-navigation
Submodule react-navigation updated 169 files
1 change: 1 addition & 0 deletions src/fabric/ModalScreenNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface NativeProps extends ViewProps {
sheetExpandsWhenScrolledToEdge?: WithDefault<boolean, false>;
customAnimationOnSwipe?: boolean;
fullScreenSwipeEnabled?: boolean;
fullScreenSwipeShadowEnabled?: boolean;
homeIndicatorHidden?: boolean;
preventNativeDismiss?: boolean;
gestureEnabled?: WithDefault<boolean, true>;
Expand Down
1 change: 1 addition & 0 deletions src/fabric/ScreenNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface NativeProps extends ViewProps {
sheetExpandsWhenScrolledToEdge?: WithDefault<boolean, false>;
customAnimationOnSwipe?: boolean;
fullScreenSwipeEnabled?: boolean;
fullScreenSwipeShadowEnabled?: boolean;
homeIndicatorHidden?: boolean;
preventNativeDismiss?: boolean;
gestureEnabled?: WithDefault<boolean, true>;
Expand Down
10 changes: 10 additions & 0 deletions src/native-stack/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ export type NativeStackNavigationOptions = {
* @platform ios
*/
fullScreenSwipeEnabled?: boolean;
/**
* 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 `false`.
*
* This does not affect the behavior of transitions that don't use gestures, enabled by `fullScreenGestureEnabled` prop.
*
* @platform ios
*/
fullScreenSwipeShadowEnabled?: boolean;
/**
* Whether you can use gestures to dismiss this screen. Defaults to `true`.
* Only supported on iOS.
Expand Down
2 changes: 2 additions & 0 deletions src/native-stack/views/NativeStackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ const RouteView = ({
}) => {
const { options, render: renderScene } = descriptors[route.key];
const {
fullScreenSwipeShadowEnabled = false,
gestureEnabled,
headerShown,
hideKeyboardOnSwipe,
Expand Down Expand Up @@ -294,6 +295,7 @@ const RouteView = ({
customAnimationOnSwipe={customAnimationOnSwipe}
freezeOnBlur={freezeOnBlur}
fullScreenSwipeEnabled={fullScreenSwipeEnabled}
fullScreenSwipeShadowEnabled={fullScreenSwipeShadowEnabled}
hideKeyboardOnSwipe={hideKeyboardOnSwipe}
homeIndicatorHidden={homeIndicatorHidden}
gestureEnabled={isAndroid ? false : gestureEnabled}
Expand Down
10 changes: 10 additions & 0 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ export interface ScreenProps extends ViewProps {
* @platform ios
*/
fullScreenSwipeEnabled?: boolean;
/**
* 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 `false`.
*
* This does not affect the behavior of transitions that don't use gestures, enabled by `fullScreenGestureEnabled` prop.
*
* @platform ios
*/
fullScreenSwipeShadowEnabled?: boolean;
/**
* Whether you can use gestures to dismiss this screen. Defaults to `true`.
*
Expand Down

0 comments on commit 776561a

Please sign in to comment.