Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(iOS): add shadow to custom push pop transitions #2239

Merged
merged 5 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -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/test-examples/src/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 @@ -90,6 +90,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
Copy link
Member

Choose a reason for hiding this comment

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

The problem with bumping react-navigation in such way is that it will be overridden by next PR that tries to do the same. We need a PR to react-navigation, merge it there to main & bump react-navigation here to new main revision.

This change should be reverted & we should wait for progress on react-navigation PR.

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
6 changes: 6 additions & 0 deletions src/native-stack/views/NativeStackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ const RouteView = ({
let {
customAnimationOnSwipe,
fullScreenSwipeEnabled,
fullScreenSwipeShadowEnabled,
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
fullScreenSwipeShadowEnabled,
fullScreenSwipeShadowEnabled = false,

gestureResponseDistance,
stackAnimation,
stackPresentation = 'push',
Expand All @@ -224,6 +225,10 @@ const RouteView = ({
}
}

if (fullScreenSwipeShadowEnabled === undefined) {
fullScreenSwipeShadowEnabled = false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (fullScreenSwipeShadowEnabled === undefined) {
fullScreenSwipeShadowEnabled = false;
}


if (index === 0) {
// first screen should always be treated as `push`, it resolves problems with no header animation
// for navigator with first screen as `modal` and the next as `push`
Expand Down Expand Up @@ -294,6 +299,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