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 2 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
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 mimick the
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
doesn't have a shadow by default. When enabled a custom shadow view is added during the transition which tries to mimick the
doesn't have a shadow by default. When enabled, a custom shadow view is added during the transition, which tries to mimick the

Copy link
Member

Choose a reason for hiding this comment

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

Same for other occourences of this comment.

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 mimick the
default iOS shadow. Defaults to `false`.

#### `gestureEnabled` (iOS only)

Whether you can use gestures to dismiss this screen. Defaults to `true`.
Expand Down
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 mimick the
* default iOS shadow. Defaults to `false`.
*
* Doesn't affect the behavior of transitions not using this gesture.
*
* @platform ios
*/
fullScreenSwipeShadowEnabled?: boolean;
/**
* Whether you can use gestures to dismiss this screen. Defaults to `true`.
* Only supported on iOS.
Expand Down
5 changes: 5 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 @@ -216,6 +217,9 @@ const RouteView = ({
if (fullScreenSwipeEnabled === undefined) {
fullScreenSwipeEnabled = true;
}
if (fullScreenSwipeShadowEnabled === undefined) {
fullScreenSwipeShadowEnabled = true;
Copy link
Member

Choose a reason for hiding this comment

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

If you do it like this, isn't it true by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking the same but fullScreenSwipeEnabled does it this way and it also is false by default. I think it is set elsewhere as false so this doesn't matter. But I can change that and maybe fullScreenSwipeEnabled also.

Copy link
Member

Choose a reason for hiding this comment

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

But the false value must be set somewhere right? Better to make this explicit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe it would be better to remove this altogether? Now I see that this is only set when swipeDirection === 'vertical' so it doesn't make sense to keep it there if it's not set for other swipe directions. Unless I should move it out of this if.

Copy link
Member

Choose a reason for hiding this comment

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

I think this prop is not dependent on any other props so we can remove it?

}
if (customAnimationOnSwipe === undefined) {
customAnimationOnSwipe = true;
}
Expand Down Expand Up @@ -294,6 +298,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 mimick the
* default iOS shadow. Defaults to `false`.
*
* Doesn't affect the behavior of transitions not using this gesture.
*
* @platform ios
*/
fullScreenSwipeShadowEnabled?: boolean;
/**
* Whether you can use gestures to dismiss this screen. Defaults to `true`.
*
Expand Down