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: add swipe direction on iOS #1260

Merged
merged 9 commits into from
Feb 8, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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 TestsExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import Test1209 from './src/Test1209';
import Test1214 from './src/Test1214';
import Test1227 from './src/Test1227';
import Test1228 from './src/Test1228';
import Test1260 from './src/Test1260';

enableFreeze(true);

Expand Down
4 changes: 2 additions & 2 deletions TestsExample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ PODS:
- React-RCTVibration
- ReactCommon/turbomodule/core
- Yoga
- RNScreens (3.10.0):
- RNScreens (3.10.1):
- React-Core
- React-RCTImage
- RNVectorIcons (7.1.0):
Expand Down Expand Up @@ -592,7 +592,7 @@ SPEC CHECKSUMS:
RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
RNReanimated: b04ef2a4f0cb61b062bbcf033f84a9e470f4f60b
RNScreens: 03ba504f8c98607ad1f07808e71040e0afa335ec
RNScreens: 522705f2e5c9d27efb17f24aceb2bf8335bc7b8e
RNVectorIcons: bc69e6a278b14842063605de32bec61f0b251a59
Yoga: c11abbf5809216c91fcd62f5571078b83d9b6720
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a
Expand Down
33 changes: 33 additions & 0 deletions TestsExample/src/Test1260.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react';
import {Button} from 'react-native';
import {NavigationContainer, ParamListBase} from '@react-navigation/native';
import {createNativeStackNavigator, NativeStackNavigationProp} from 'react-native-screens/native-stack';

const Stack = createNativeStackNavigator();

export default function App() {
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{swipeDirection: 'vertical'}}>
<Stack.Screen name="First" component={First} />
<Stack.Screen
name="Second"
component={Second}
/>
</Stack.Navigator>
</NavigationContainer>
);
}

function First({navigation}: {navigation: NativeStackNavigationProp<ParamListBase>}) {
return (
<Button title="Tap me for second screen" onPress={() => navigation.navigate('Second')} />

);
}

function Second({navigation}: {navigation: NativeStackNavigationProp<ParamListBase>}) {
return (
<Button title="Tap me for second screen" onPress={() => navigation.navigate('First')} />
);
}
8 changes: 8 additions & 0 deletions guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ Defaults to `auto`.

Sets the translucency of the status bar (similar to the `StatusBar` component). Defaults to `false`.

#### `swipeDirection` (iOS only)

Sets the direction in which you should swipe to dismiss the screen. The following values are supported:
- `vertical` – dismiss screen vertically
- `horizontal` – dismiss screen horizontally (default)
kacperkapusciak marked this conversation as resolved.
Show resolved Hide resolved

When using `vertical` option, `fullScreenSwipeEnabled: true`, `customAnimationOnSwipe: true` and `stackAnimation: 'slide_from_bottom'` should be set by default.

#### `useTransitionProgress`

Hook providing context value of transition progress of the current screen to be used with `react-native` `Animated`. It consists of 2 values:
Expand Down
6 changes: 6 additions & 0 deletions ios/RNSScreen.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ typedef NS_ENUM(NSInteger, RNSScreenReplaceAnimation) {
RNSScreenReplaceAnimationPush,
};

typedef NS_ENUM(NSInteger, RNSScreenSwipeDirection) {
RNSScreenSwipeDirectionVertical,
RNSScreenSwipeDirectionHorizontal,
};

typedef NS_ENUM(NSInteger, RNSActivityState) {
RNSActivityStateInactive = 0,
RNSActivityStateTransitioningOrBelowTop = 1,
Expand Down Expand Up @@ -91,6 +96,7 @@ typedef NS_ENUM(NSInteger, RNSWindowTrait) {
@property (nonatomic) RNSScreenStackAnimation stackAnimation;
@property (nonatomic) RNSScreenStackPresentation stackPresentation;
@property (nonatomic) RNSScreenReplaceAnimation replaceAnimation;
@property (nonatomic) RNSScreenSwipeDirection swipeDirection;
@property (nonatomic) BOOL preventNativeDismiss;
@property (nonatomic) BOOL hasOrientationSet;
@property (nonatomic) BOOL hasStatusBarStyleSet;
Expand Down
10 changes: 10 additions & 0 deletions ios/RNSScreen.m
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,7 @@ @implementation RNSScreenManager
RCT_EXPORT_VIEW_PROPERTY(replaceAnimation, RNSScreenReplaceAnimation)
RCT_EXPORT_VIEW_PROPERTY(stackPresentation, RNSScreenStackPresentation)
RCT_EXPORT_VIEW_PROPERTY(stackAnimation, RNSScreenStackAnimation)
RCT_EXPORT_VIEW_PROPERTY(swipeDirection, RNSScreenSwipeDirection)

RCT_EXPORT_VIEW_PROPERTY(onAppear, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onDisappear, RCTDirectEventBlock);
Expand Down Expand Up @@ -835,6 +836,15 @@ @implementation RCTConvert (RNSScreen)
RNSScreenReplaceAnimationPop,
integerValue)

RCT_ENUM_CONVERTER(
RNSScreenSwipeDirection,
(@{
@"vertical" : @(RNSScreenSwipeDirectionVertical),
@"horizontal" : @(RNSScreenSwipeDirectionHorizontal),
}),
RNSScreenSwipeDirectionHorizontal,
integerValue)

#if !TARGET_OS_TV
RCT_ENUM_CONVERTER(
RNSStatusBarStyle,
Expand Down
76 changes: 67 additions & 9 deletions ios/RNSScreenStack.m
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,16 @@ @interface RNSScreenEdgeGestureRecognizer : UIScreenEdgePanGestureRecognizer
@implementation RNSScreenEdgeGestureRecognizer
@end

@interface RNSPanGestureRecognizer : UIPanGestureRecognizer
@interface RNSPanGestureRecognizerVertical : UIPanGestureRecognizer
@end

@implementation RNSPanGestureRecognizer
@interface RNSPanGestureRecognizerHorizontal : UIPanGestureRecognizer
@end

@implementation RNSPanGestureRecognizerVertical
@end

@implementation RNSPanGestureRecognizerHorizontal
@end
#endif

Expand Down Expand Up @@ -584,7 +590,10 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
if (topScreen.fullScreenSwipeEnabled) {
// we want only `RNSPanGestureRecognizer` to be able to recognize when
// `fullScreenSwipeEnabled` is set
if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
if (([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizerHorizontal class]] &&
topScreen.swipeDirection == RNSScreenSwipeDirectionHorizontal) ||
([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizerVertical class]] &&
Copy link
Member

Choose a reason for hiding this comment

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

As vertical swipe doesn't really make sense without fullScreenSwipeEnabled:

Screen.Recording.2022-01-26.at.09.48.44.mov

we should set it to true by default when developer sets swipeDirection to vertical

topScreen.swipeDirection == RNSScreenSwipeDirectionVertical)) {
_isFullWidthSwiping = YES;
[self cancelTouchesInParent];
return YES;
Expand All @@ -610,7 +619,9 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
if ([gestureRecognizer isKindOfClass:[RNSScreenEdgeGestureRecognizer class]]) {
// it should only recognize with `customAnimationOnSwipe` set
return NO;
} else if ([gestureRecognizer isKindOfClass:[RNSPanGestureRecognizer class]]) {
} else if (
[gestureRecognizer isKindOfClass:[RNSPanGestureRecognizerHorizontal class]] ||
[gestureRecognizer isKindOfClass:[RNSPanGestureRecognizerVertical class]]) {
// it should only recognize with `fullScreenSwipeEnabled` set
return NO;
}
Expand All @@ -636,11 +647,16 @@ - (void)setupGestureHandlers
rightEdgeSwipeGestureRecognizer.delegate = self;
[self addGestureRecognizer:rightEdgeSwipeGestureRecognizer];

// gesture recognizer for full width swipe gesture
RNSPanGestureRecognizer *panRecognizer = [[RNSPanGestureRecognizer alloc] initWithTarget:self
action:@selector(handleSwipe:)];
panRecognizer.delegate = self;
[self addGestureRecognizer:panRecognizer];
// gesture recognizers for full width swipe gesture
RNSPanGestureRecognizerHorizontal *panRecognizerHorizontal =
[[RNSPanGestureRecognizerHorizontal alloc] initWithTarget:self action:@selector(handleSwipe:)];
RNSPanGestureRecognizerVertical *panRecognizerVertical =
[[RNSPanGestureRecognizerVertical alloc] initWithTarget:self action:@selector(handleSwipeVertical:)];

panRecognizerHorizontal.delegate = self;
[self addGestureRecognizer:panRecognizerHorizontal];
panRecognizerVertical.delegate = self;
[self addGestureRecognizer:panRecognizerVertical];
}

- (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
Expand Down Expand Up @@ -689,6 +705,48 @@ - (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
}
}
}

- (void)handleSwipeVertical:(UIPanGestureRecognizer *)gestureRecognizer
{
float translation = [gestureRecognizer translationInView:gestureRecognizer.view].y;
float velocity = [gestureRecognizer velocityInView:gestureRecognizer.view].y;
float distance = gestureRecognizer.view.bounds.size.height;

float transitionProgress = (translation / distance);

switch (gestureRecognizer.state) {
case UIGestureRecognizerStateBegan: {
_interactionController = [UIPercentDrivenInteractiveTransition new];
[_controller popViewControllerAnimated:YES];
break;
}

case UIGestureRecognizerStateChanged: {
[_interactionController updateInteractiveTransition:transitionProgress];
break;
}

case UIGestureRecognizerStateCancelled: {
[_interactionController cancelInteractiveTransition];
break;
}

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);
if (shouldFinishTransition) {
[_interactionController finishInteractiveTransition];
} else {
[_interactionController cancelInteractiveTransition];
}
_interactionController = nil;
}
default: {
break;
}
}
}
#endif

- (id<UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
Expand Down
32 changes: 24 additions & 8 deletions ios/RNSScreenStackAnimator.m
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,30 @@ - (void)animateSlideFromBottomWithTransitionContext:(id<UIViewControllerContextT
} else if (_operation == UINavigationControllerOperationPop) {
toViewController.view.transform = CGAffineTransformIdentity;
[[transitionContext containerView] insertSubview:toViewController.view belowSubview:fromViewController.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext]
animations:^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = topBottomTransform;
}
completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];

void (^animationBlock)(void) = ^{
toViewController.view.transform = CGAffineTransformIdentity;
fromViewController.view.transform = topBottomTransform;
};
void (^completionBlock)(BOOL) = ^(BOOL finished) {
if (transitionContext.transitionWasCancelled) {
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];
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions native-stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,14 @@ Defaults to `push`.

Using `containedModal` and `containedTransparentModal` with other types of modals in one native stack navigator is not recommended and can result in a freeze or a crash of the application.

#### `swipeDirection` (iOS only)

Sets the direction in which you should swipe to dismiss the screen. The following values are supported:
- `vertical` – dismiss screen vertically
- `horizontal` – dismiss screen horizontally (default)

When using `vertical` option, `fullScreenSwipeEnabled: true`, `customAnimationOnSwipe: true` and `stackAnimation: 'slide_from_bottom'` will be set by default.

#### `title`

A string that can be used as a fallback for `headerTitle`.
Expand Down
9 changes: 9 additions & 0 deletions src/native-stack/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,15 @@ export type NativeStackNavigationOptions = {
* @platform android
*/
statusBarTranslucent?: boolean;
/**
* Sets the direction in which you should swipe to dismiss the screen.
* When using `vertical` option, `fullScreenSwipeEnabled: true`, `customAnimationOnSwipe: true` and `stackAnimation: 'slide_from_bottom'` will be set by default.
* The following values are supported:
* - `vertical` – dismiss screen vertically
* - `horizontal` – dismiss screen horizontally (default)
* @platform ios
*/
swipeDirection?: ScreenProps['swipeDirection'];
/**
* String that can be displayed in the header as a fallback for `headerTitle`.
*/
Expand Down
29 changes: 25 additions & 4 deletions src/native-stack/views/NativeStackView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,22 +149,42 @@ const RouteView = ({
}) => {
const { options, render: renderScene } = descriptors[route.key];
const {
customAnimationOnSwipe,
fullScreenSwipeEnabled,
gestureEnabled,
headerShown,
nativeBackButtonDismissalEnabled = false,
replaceAnimation = 'pop',
screenOrientation,
stackAnimation,
statusBarAnimation,
statusBarColor,
statusBarHidden,
statusBarStyle,
statusBarTranslucent,
swipeDirection = 'horizontal',
} = options;

let {
customAnimationOnSwipe,
fullScreenSwipeEnabled,
stackAnimation,
stackPresentation = 'push',
} = options;

let { stackPresentation = 'push' } = options;
if (swipeDirection === 'vertical') {
// for `vertical` direction to work, we need to set `fullScreenSwipeEnabled` to `true`
// so the screen can be dismissed from any point on screen.
// `customAnimationOnSwipe` needs to be set to `true` so the `stackAnimation` set by user can be used,
// otherwise `simple_push` will be used.
// Also, the default animation for this direction seems to be `slide_from_bottom`.
if (fullScreenSwipeEnabled === undefined) {
fullScreenSwipeEnabled = true;
}
if (customAnimationOnSwipe === undefined) {
customAnimationOnSwipe = true;
}
kacperkapusciak marked this conversation as resolved.
Show resolved Hide resolved
if (stackAnimation === undefined) {
stackAnimation = 'slide_from_bottom';
}
}

if (index === 0) {
// first screen should always be treated as `push`, it resolves problems with no header animation
Expand Down Expand Up @@ -205,6 +225,7 @@ const RouteView = ({
statusBarHidden={statusBarHidden}
statusBarStyle={statusBarStyle}
statusBarTranslucent={statusBarTranslucent}
swipeDirection={swipeDirection}
onHeaderBackButtonClicked={() => {
navigation.dispatch({
...StackActions.pop(),
Expand Down
10 changes: 10 additions & 0 deletions src/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export type BlurEffectTypes =
| 'systemThickMaterialDark'
| 'systemChromeMaterialDark';
export type ScreenReplaceTypes = 'push' | 'pop';
export type SwipeDirectionTypes = 'vertical' | 'horizontal';
export type ScreenOrientationTypes =
| 'default'
| 'all'
Expand Down Expand Up @@ -214,6 +215,15 @@ export interface ScreenProps extends ViewProps {
* @platform android
*/
statusBarTranslucent?: boolean;
/**
* Sets the direction in which you should swipe to dismiss the screen.
* When using `vertical` option, `fullScreenSwipeEnabled: true`, `customAnimationOnSwipe: true` and `stackAnimation: 'slide_from_bottom'` should be set by default.
* The following values are supported:
* - `vertical` – dismiss screen vertically
* - `horizontal` – dismiss screen horizontally (default)
* @platform ios
*/
swipeDirection?: SwipeDirectionTypes;
}

export interface ScreenContainerProps extends ViewProps {
Expand Down