Skip to content

Commit

Permalink
feat(iOS): Add slide_from_left transition (#2057)
Browse files Browse the repository at this point in the history
## Description

Before `slide_from_left` transition was resolved to default transition
on iOS. Now this transition will make screen to appear from left side to
right.

## Changes

- updated docs;
- added `RNSScreenStackAnimationSlideFromLeft` to enum value;
- added `animateSlideFromLeftWithTransitionContext` method;
- added support for reverse gestures (from right to left to close a
screen);

## Screenshots / GIFs


https://github.com/software-mansion/react-native-screens/assets/22820318/e0a71147-0aea-47ef-9d4b-319d7fd8dd81

## Test code and steps to reproduce

- Open `Animations` screen
- select `slide_from_left` animation
- open "New screen"
- if you want to test gesture for dismissing the screen, you'll need to
specify `<Stack.Screen name="Screen" options={{ customAnimationOnSwipe:
true }}>`

## Checklist

- [x] Included code example that can be used to test this change
- [x] 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
- [x] Ensured that CI passes
  • Loading branch information
kirillzyusko authored and tboba committed Mar 25, 2024
1 parent 8458671 commit 307e5ff
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 12 deletions.
2 changes: 1 addition & 1 deletion guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ Allows for the customization of how the given screen should appear/disappear whe
- `"simple_push"` – performs a default animation, but without shadow and native header transition (iOS only)
- `"slide_from_bottom"` - slide in the new screen from bottom to top
- `"slide_from_right"` - slide in the new screen from right to left (Android only, resolves to default transition on iOS)
- `"slide_from_left"` - slide in the new screen from left to right (Android only, resolves to default transition on iOS)
- `"slide_from_left"` - slide in the new screen from left to right
- `"ios"` - iOS like slide in animation (Android only, resolves to default transition on iOS)
- `"none"` – the screen appears/disappears without an animation

Expand Down
5 changes: 3 additions & 2 deletions ios/RNSConvert.mm
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ + (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent:
+ (RNSScreenStackAnimation)RNSScreenStackAnimationFromCppEquivalent:(react::RNSScreenStackAnimation)stackAnimation
{
switch (stackAnimation) {
// these four are intentionally grouped
// these three are intentionally grouped
case react::RNSScreenStackAnimation::Slide_from_right:
case react::RNSScreenStackAnimation::Slide_from_left:
case react::RNSScreenStackAnimation::Ios:
case react::RNSScreenStackAnimation::Default:
return RNSScreenStackAnimationDefault;
case react::RNSScreenStackAnimation::Slide_from_left:
return RNSScreenStackAnimationSlideFromLeft;
case react::RNSScreenStackAnimation::Flip:
return RNSScreenStackAnimationFlip;
case react::RNSScreenStackAnimation::Simple_push:
Expand Down
1 change: 1 addition & 0 deletions ios/RNSEnums.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
RNSScreenStackAnimationFlip,
RNSScreenStackAnimationSlideFromBottom,
RNSScreenStackAnimationSimplePush,
RNSScreenStackAnimationSlideFromLeft,
};

typedef NS_ENUM(NSInteger, RNSScreenReplaceAnimation) {
Expand Down
3 changes: 2 additions & 1 deletion ios/RNSScreen.mm
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ - (void)setStackAnimation:(RNSScreenStackAnimation)stackAnimation
case RNSScreenStackAnimationSimplePush:
case RNSScreenStackAnimationSlideFromBottom:
case RNSScreenStackAnimationFadeFromBottom:
case RNSScreenStackAnimationSlideFromLeft:
// Default
break;
}
Expand Down Expand Up @@ -1572,7 +1573,7 @@ @implementation RCTConvert (RNSScreen)
@"simple_push" : @(RNSScreenStackAnimationSimplePush),
@"slide_from_bottom" : @(RNSScreenStackAnimationSlideFromBottom),
@"slide_from_right" : @(RNSScreenStackAnimationDefault),
@"slide_from_left" : @(RNSScreenStackAnimationDefault),
@"slide_from_left" : @(RNSScreenStackAnimationSlideFromLeft),
@"ios" : @(RNSScreenStackAnimationDefault),
}),
RNSScreenStackAnimationDefault,
Expand Down
18 changes: 13 additions & 5 deletions ios/RNSScreenStack.mm
Original file line number Diff line number Diff line change
Expand Up @@ -749,12 +749,14 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
// Now we're dealing with RNSScreenEdgeGestureRecognizer (or _UIParallaxTransitionPanGestureRecognizer)
if (topScreen.customAnimationOnSwipe && [RNSScreenStackAnimator isCustomAnimation:topScreen.stackAnimation]) {
if ([gestureRecognizer isKindOfClass:[RNSScreenEdgeGestureRecognizer class]]) {
UIRectEdge edges = ((RNSScreenEdgeGestureRecognizer *)gestureRecognizer).edges;
BOOL isRTL = _controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft;
BOOL isSlideFromLeft = topScreen.stackAnimation == RNSScreenStackAnimationSlideFromLeft;
// if we do not set any explicit `semanticContentAttribute`, it is `UISemanticContentAttributeUnspecified` instead
// of `UISemanticContentAttributeForceLeftToRight`, so we just check if it is RTL or not
BOOL isCorrectEdge = (_controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft &&
((RNSScreenEdgeGestureRecognizer *)gestureRecognizer).edges == UIRectEdgeRight) ||
(_controller.view.semanticContentAttribute != UISemanticContentAttributeForceRightToLeft &&
((RNSScreenEdgeGestureRecognizer *)gestureRecognizer).edges == UIRectEdgeLeft);
BOOL isCorrectEdge = (isRTL && edges == UIRectEdgeRight) ||
(!isRTL && isSlideFromLeft && edges == UIRectEdgeRight) ||
(isRTL && isSlideFromLeft && edges == UIRectEdgeLeft) || (!isRTL && edges == UIRectEdgeLeft);
if (isCorrectEdge) {
[self cancelTouchesInParent];
return YES;
Expand Down Expand Up @@ -818,7 +820,10 @@ - (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
}
}

bool isInverted = topScreen.stackAnimation == RNSScreenStackAnimationSlideFromLeft;

float transitionProgress = (translation / distance);
transitionProgress = isInverted ? transitionProgress * -1 : transitionProgress;

switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan: {
Expand All @@ -840,7 +845,10 @@ - (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
case UIGestureRecognizerStateEnded: {
// values taken from
// https://github.com/react-navigation/react-navigation/blob/54739828598d7072c1bf7b369659e3682db3edc5/packages/stack/src/views/Stack/Card.tsx#L316
BOOL shouldFinishTransition = (translation + velocity * 0.3) > (distance / 2);
float snapPoint = distance / 2;
float gestureDistance = translation + velocity * 0.3;
gestureDistance = isInverted ? gestureDistance * -1 : gestureDistance;
BOOL shouldFinishTransition = gestureDistance > snapPoint;
if (shouldFinishTransition) {
[_interactionController finishInteractiveTransition];
} else {
Expand Down
61 changes: 61 additions & 0 deletions ios/RNSScreenStackAnimator.mm
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,64 @@ - (void)animateSimplePushWithTransitionContext:(id<UIViewControllerContextTransi
}
}

- (void)animateSlideFromLeftWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
{
float containerWidth = transitionContext.containerView.bounds.size.width;
float belowViewWidth = containerWidth * 0.3;

CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0);
CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0);

if (toViewController.navigationController.view.semanticContentAttribute ==
UISemanticContentAttributeForceRightToLeft) {
rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0);
leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0);
}

if (_operation == UINavigationControllerOperationPush) {
toViewController.view.transform = rightTransform;
[[transitionContext containerView] addSubview:toViewController.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
fromViewController.view.transform = leftTransform;
toViewController.view.transform = CGAffineTransformIdentity;
}
completion:^(BOOL finished) {
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];

void (^animationBlock)(void) = ^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = rightTransform;
};
void (^completionBlock)(BOOL) = ^(BOOL finished) {
fromViewController.view.transform = CGAffineTransformIdentity;
toViewController.view.transform = CGAffineTransformIdentity;
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
};

if (!transitionContext.isInteractive) {
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:animationBlock
completion:completionBlock];
} else {
// we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
[UIView animateWithDuration:[self transitionDuration:transitionContext]
delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:animationBlock
completion:completionBlock];
}
}
}

- (void)animateFadeWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
toVC:(UIViewController *)toViewController
fromVC:(UIViewController *)fromViewController
Expand Down Expand Up @@ -330,6 +388,9 @@ - (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation
if (animation == RNSScreenStackAnimationSimplePush) {
[self animateSimplePushWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
} else if (animation == RNSScreenStackAnimationSlideFromLeft) {
[self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
} else if (animation == RNSScreenStackAnimationFade || animation == RNSScreenStackAnimationNone) {
[self animateFadeWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
return;
Expand Down
2 changes: 1 addition & 1 deletion native-stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ How the given screen should appear/disappear when pushed or popped at the top of
- `simple_push` – performs a default animation, but without shadow and native header transition (iOS only)
- `slide_from_bottom` – performs a slide from bottom animation
- `slide_from_right` - slide in the new screen from right to left (Android only, resolves to default transition on iOS)
- `slide_from_left` - slide in the new screen from left to right (Android only, resolves to default transition on iOS)
- `slide_from_left` - slide in the new screen from left to right
- `ios` - iOS like slide in animation (Android only, resolves to default transition on iOS)
- `none` - the screen appears/disappears without an animation.

Expand Down
2 changes: 1 addition & 1 deletion src/native-stack/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ export type NativeStackNavigationOptions = {
* - "simple_push" – performs a default animation, but without shadow and native header transition (iOS only)
* - "slide_from_bottom" – performs a slide from bottom animation
* - "slide_from_right" - slide in the new screen from right to left (Android only, resolves to default transition on iOS)
* - "slide_from_left" - slide in the new screen from left to right (Android only, resolves to default transition on iOS)
* - "slide_from_left" - slide in the new screen from left to right
* - "ios" - iOS like slide in animation (Android only, resolves to default transition on iOS)
* - "none" – the screen appears/dissapears without an animation
*/
Expand Down
2 changes: 1 addition & 1 deletion src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ export interface ScreenProps extends ViewProps {
* - "simple_push" – performs a default animation, but without shadow and native header transition (iOS only)
* - `slide_from_bottom` – performs a slide from bottom animation
* - "slide_from_right" - slide in the new screen from right to left (Android only, resolves to default transition on iOS)
* - "slide_from_left" - slide in the new screen from left to right (Android only, resolves to default transition on iOS)
* - "slide_from_left" - slide in the new screen from left to right
* - "ios" - iOS like slide in animation (Android only, resolves to default transition on iOS)
* - "none" – the screen appears/dissapears without an animation
*/
Expand Down

0 comments on commit 307e5ff

Please sign in to comment.