Skip to content

Commit 307e5ff

Browse files
kirillzyuskotboba
authored andcommitted
feat(iOS): Add slide_from_left transition (#2057)
## 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
1 parent 8458671 commit 307e5ff

File tree

9 files changed

+84
-12
lines changed

9 files changed

+84
-12
lines changed

guides/GUIDE_FOR_LIBRARY_AUTHORS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ Allows for the customization of how the given screen should appear/disappear whe
202202
- `"simple_push"` – performs a default animation, but without shadow and native header transition (iOS only)
203203
- `"slide_from_bottom"` - slide in the new screen from bottom to top
204204
- `"slide_from_right"` - slide in the new screen from right to left (Android only, resolves to default transition on iOS)
205-
- `"slide_from_left"` - slide in the new screen from left to right (Android only, resolves to default transition on iOS)
205+
- `"slide_from_left"` - slide in the new screen from left to right
206206
- `"ios"` - iOS like slide in animation (Android only, resolves to default transition on iOS)
207207
- `"none"` – the screen appears/disappears without an animation
208208

ios/RNSConvert.mm

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ + (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent:
2727
+ (RNSScreenStackAnimation)RNSScreenStackAnimationFromCppEquivalent:(react::RNSScreenStackAnimation)stackAnimation
2828
{
2929
switch (stackAnimation) {
30-
// these four are intentionally grouped
30+
// these three are intentionally grouped
3131
case react::RNSScreenStackAnimation::Slide_from_right:
32-
case react::RNSScreenStackAnimation::Slide_from_left:
3332
case react::RNSScreenStackAnimation::Ios:
3433
case react::RNSScreenStackAnimation::Default:
3534
return RNSScreenStackAnimationDefault;
35+
case react::RNSScreenStackAnimation::Slide_from_left:
36+
return RNSScreenStackAnimationSlideFromLeft;
3637
case react::RNSScreenStackAnimation::Flip:
3738
return RNSScreenStackAnimationFlip;
3839
case react::RNSScreenStackAnimation::Simple_push:

ios/RNSEnums.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ typedef NS_ENUM(NSInteger, RNSScreenStackAnimation) {
1616
RNSScreenStackAnimationFlip,
1717
RNSScreenStackAnimationSlideFromBottom,
1818
RNSScreenStackAnimationSimplePush,
19+
RNSScreenStackAnimationSlideFromLeft,
1920
};
2021

2122
typedef NS_ENUM(NSInteger, RNSScreenReplaceAnimation) {

ios/RNSScreen.mm

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ - (void)setStackAnimation:(RNSScreenStackAnimation)stackAnimation
199199
case RNSScreenStackAnimationSimplePush:
200200
case RNSScreenStackAnimationSlideFromBottom:
201201
case RNSScreenStackAnimationFadeFromBottom:
202+
case RNSScreenStackAnimationSlideFromLeft:
202203
// Default
203204
break;
204205
}
@@ -1572,7 +1573,7 @@ @implementation RCTConvert (RNSScreen)
15721573
@"simple_push" : @(RNSScreenStackAnimationSimplePush),
15731574
@"slide_from_bottom" : @(RNSScreenStackAnimationSlideFromBottom),
15741575
@"slide_from_right" : @(RNSScreenStackAnimationDefault),
1575-
@"slide_from_left" : @(RNSScreenStackAnimationDefault),
1576+
@"slide_from_left" : @(RNSScreenStackAnimationSlideFromLeft),
15761577
@"ios" : @(RNSScreenStackAnimationDefault),
15771578
}),
15781579
RNSScreenStackAnimationDefault,

ios/RNSScreenStack.mm

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -749,12 +749,14 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
749749
// Now we're dealing with RNSScreenEdgeGestureRecognizer (or _UIParallaxTransitionPanGestureRecognizer)
750750
if (topScreen.customAnimationOnSwipe && [RNSScreenStackAnimator isCustomAnimation:topScreen.stackAnimation]) {
751751
if ([gestureRecognizer isKindOfClass:[RNSScreenEdgeGestureRecognizer class]]) {
752+
UIRectEdge edges = ((RNSScreenEdgeGestureRecognizer *)gestureRecognizer).edges;
753+
BOOL isRTL = _controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft;
754+
BOOL isSlideFromLeft = topScreen.stackAnimation == RNSScreenStackAnimationSlideFromLeft;
752755
// if we do not set any explicit `semanticContentAttribute`, it is `UISemanticContentAttributeUnspecified` instead
753756
// of `UISemanticContentAttributeForceLeftToRight`, so we just check if it is RTL or not
754-
BOOL isCorrectEdge = (_controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft &&
755-
((RNSScreenEdgeGestureRecognizer *)gestureRecognizer).edges == UIRectEdgeRight) ||
756-
(_controller.view.semanticContentAttribute != UISemanticContentAttributeForceRightToLeft &&
757-
((RNSScreenEdgeGestureRecognizer *)gestureRecognizer).edges == UIRectEdgeLeft);
757+
BOOL isCorrectEdge = (isRTL && edges == UIRectEdgeRight) ||
758+
(!isRTL && isSlideFromLeft && edges == UIRectEdgeRight) ||
759+
(isRTL && isSlideFromLeft && edges == UIRectEdgeLeft) || (!isRTL && edges == UIRectEdgeLeft);
758760
if (isCorrectEdge) {
759761
[self cancelTouchesInParent];
760762
return YES;
@@ -818,7 +820,10 @@ - (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
818820
}
819821
}
820822

823+
bool isInverted = topScreen.stackAnimation == RNSScreenStackAnimationSlideFromLeft;
824+
821825
float transitionProgress = (translation / distance);
826+
transitionProgress = isInverted ? transitionProgress * -1 : transitionProgress;
822827

823828
switch (gestureRecognizer.state) {
824829
case UIGestureRecognizerStateBegan: {
@@ -840,7 +845,10 @@ - (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
840845
case UIGestureRecognizerStateEnded: {
841846
// values taken from
842847
// https://github.com/react-navigation/react-navigation/blob/54739828598d7072c1bf7b369659e3682db3edc5/packages/stack/src/views/Stack/Card.tsx#L316
843-
BOOL shouldFinishTransition = (translation + velocity * 0.3) > (distance / 2);
848+
float snapPoint = distance / 2;
849+
float gestureDistance = translation + velocity * 0.3;
850+
gestureDistance = isInverted ? gestureDistance * -1 : gestureDistance;
851+
BOOL shouldFinishTransition = gestureDistance > snapPoint;
844852
if (shouldFinishTransition) {
845853
[_interactionController finishInteractiveTransition];
846854
} else {

ios/RNSScreenStackAnimator.mm

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,64 @@ - (void)animateSimplePushWithTransitionContext:(id<UIViewControllerContextTransi
147147
}
148148
}
149149

150+
- (void)animateSlideFromLeftWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
151+
toVC:(UIViewController *)toViewController
152+
fromVC:(UIViewController *)fromViewController
153+
{
154+
float containerWidth = transitionContext.containerView.bounds.size.width;
155+
float belowViewWidth = containerWidth * 0.3;
156+
157+
CGAffineTransform rightTransform = CGAffineTransformMakeTranslation(-containerWidth, 0);
158+
CGAffineTransform leftTransform = CGAffineTransformMakeTranslation(belowViewWidth, 0);
159+
160+
if (toViewController.navigationController.view.semanticContentAttribute ==
161+
UISemanticContentAttributeForceRightToLeft) {
162+
rightTransform = CGAffineTransformMakeTranslation(containerWidth, 0);
163+
leftTransform = CGAffineTransformMakeTranslation(-belowViewWidth, 0);
164+
}
165+
166+
if (_operation == UINavigationControllerOperationPush) {
167+
toViewController.view.transform = rightTransform;
168+
[[transitionContext containerView] addSubview:toViewController.view];
169+
[UIView animateWithDuration:[self transitionDuration:transitionContext]
170+
animations:^{
171+
fromViewController.view.transform = leftTransform;
172+
toViewController.view.transform = CGAffineTransformIdentity;
173+
}
174+
completion:^(BOOL finished) {
175+
fromViewController.view.transform = CGAffineTransformIdentity;
176+
toViewController.view.transform = CGAffineTransformIdentity;
177+
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
178+
}];
179+
} else if (_operation == UINavigationControllerOperationPop) {
180+
toViewController.view.transform = leftTransform;
181+
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
182+
183+
void (^animationBlock)(void) = ^{
184+
toViewController.view.transform = CGAffineTransformIdentity;
185+
fromViewController.view.transform = rightTransform;
186+
};
187+
void (^completionBlock)(BOOL) = ^(BOOL finished) {
188+
fromViewController.view.transform = CGAffineTransformIdentity;
189+
toViewController.view.transform = CGAffineTransformIdentity;
190+
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
191+
};
192+
193+
if (!transitionContext.isInteractive) {
194+
[UIView animateWithDuration:[self transitionDuration:transitionContext]
195+
animations:animationBlock
196+
completion:completionBlock];
197+
} else {
198+
// we don't want the EaseInOut option when swiping to dismiss the view, it is the same in default animation option
199+
[UIView animateWithDuration:[self transitionDuration:transitionContext]
200+
delay:0.0
201+
options:UIViewAnimationOptionCurveLinear
202+
animations:animationBlock
203+
completion:completionBlock];
204+
}
205+
}
206+
}
207+
150208
- (void)animateFadeWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext
151209
toVC:(UIViewController *)toViewController
152210
fromVC:(UIViewController *)fromViewController
@@ -330,6 +388,9 @@ - (void)animateTransitionWithStackAnimation:(RNSScreenStackAnimation)animation
330388
if (animation == RNSScreenStackAnimationSimplePush) {
331389
[self animateSimplePushWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
332390
return;
391+
} else if (animation == RNSScreenStackAnimationSlideFromLeft) {
392+
[self animateSlideFromLeftWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
393+
return;
333394
} else if (animation == RNSScreenStackAnimationFade || animation == RNSScreenStackAnimationNone) {
334395
[self animateFadeWithTransitionContext:transitionContext toVC:toVC fromVC:fromVC];
335396
return;

native-stack/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ How the given screen should appear/disappear when pushed or popped at the top of
286286
- `simple_push` – performs a default animation, but without shadow and native header transition (iOS only)
287287
- `slide_from_bottom` – performs a slide from bottom animation
288288
- `slide_from_right` - slide in the new screen from right to left (Android only, resolves to default transition on iOS)
289-
- `slide_from_left` - slide in the new screen from left to right (Android only, resolves to default transition on iOS)
289+
- `slide_from_left` - slide in the new screen from left to right
290290
- `ios` - iOS like slide in animation (Android only, resolves to default transition on iOS)
291291
- `none` - the screen appears/disappears without an animation.
292292

src/native-stack/types.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ export type NativeStackNavigationOptions = {
392392
* - "simple_push" – performs a default animation, but without shadow and native header transition (iOS only)
393393
* - "slide_from_bottom" – performs a slide from bottom animation
394394
* - "slide_from_right" - slide in the new screen from right to left (Android only, resolves to default transition on iOS)
395-
* - "slide_from_left" - slide in the new screen from left to right (Android only, resolves to default transition on iOS)
395+
* - "slide_from_left" - slide in the new screen from left to right
396396
* - "ios" - iOS like slide in animation (Android only, resolves to default transition on iOS)
397397
* - "none" – the screen appears/dissapears without an animation
398398
*/

src/types.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ export interface ScreenProps extends ViewProps {
322322
* - "simple_push" – performs a default animation, but without shadow and native header transition (iOS only)
323323
* - `slide_from_bottom` – performs a slide from bottom animation
324324
* - "slide_from_right" - slide in the new screen from right to left (Android only, resolves to default transition on iOS)
325-
* - "slide_from_left" - slide in the new screen from left to right (Android only, resolves to default transition on iOS)
325+
* - "slide_from_left" - slide in the new screen from left to right
326326
* - "ios" - iOS like slide in animation (Android only, resolves to default transition on iOS)
327327
* - "none" – the screen appears/dissapears without an animation
328328
*/

0 commit comments

Comments
 (0)