Skip to content

Commit

Permalink
feat: add swipe direction on iOS (#1260)
Browse files Browse the repository at this point in the history
Added swipeDirection prop on iOS. Requested in #946

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

Co-authored-by: kacperkapusciak <kacper.kapusciak@swmansion.com>
  • Loading branch information
WoLewicki and kacperkapusciak authored Feb 8, 2022
1 parent e27baec commit f2ac36b
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 19 deletions.
1 change: 1 addition & 0 deletions TestsExample/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import Test1214 from './src/Test1214';
import Test1227 from './src/Test1227';
import Test1228 from './src/Test1228';
import Test1259 from './src/Test1259';
import Test1260 from './src/Test1260';

enableFreeze(true);

Expand Down
37 changes: 37 additions & 0 deletions TestsExample/src/Test1260.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as React from 'react';
import {Button, View} 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 (
<View style={{flex: 1, backgroundColor: 'red'}}>
<Button title="Tap me for second screen" onPress={() => navigation.navigate('Second')} />
</View>

);
}

function Second({navigation}: {navigation: NativeStackNavigationProp<ParamListBase>}) {
return (
<View style={{flex: 1, backgroundColor: 'blue'}}>
<Button title="Tap me for second screen" onPress={() => navigation.navigate('First')} />
</View>
);
}
8 changes: 8 additions & 0 deletions guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,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)

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

### `transitionDuration` (iOS only)

Changes the duration (in milliseconds) of `slide_from_bottom`, `fade_from_bottom`, `fade` and `simple_push` transitions on iOS. Defaults to `350`.
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 @@ -92,6 +97,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 @@ -793,6 +793,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(transitionDuration, NSNumber)

RCT_EXPORT_VIEW_PROPERTY(onAppear, RCTDirectEventBlock);
Expand Down Expand Up @@ -859,6 +860,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
24 changes: 17 additions & 7 deletions ios/RNSScreenStack.m
Original file line number Diff line number Diff line change
Expand Up @@ -650,13 +650,23 @@ - (void)setupGestureHandlers

- (void)handleSwipe:(UIPanGestureRecognizer *)gestureRecognizer
{
float translation = [gestureRecognizer translationInView:gestureRecognizer.view].x;
float velocity = [gestureRecognizer velocityInView:gestureRecognizer.view].x;
float distance = gestureRecognizer.view.bounds.size.width;
BOOL isRTL = _controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft;
if (isRTL) {
translation = -translation;
velocity = -velocity;
RNSScreenView *topScreen = (RNSScreenView *)_controller.viewControllers.lastObject.view;
float translation;
float velocity;
float distance;
if (topScreen.swipeDirection == RNSScreenSwipeDirectionVertical) {
translation = [gestureRecognizer translationInView:gestureRecognizer.view].y;
velocity = [gestureRecognizer velocityInView:gestureRecognizer.view].y;
distance = gestureRecognizer.view.bounds.size.height;
} else {
translation = [gestureRecognizer translationInView:gestureRecognizer.view].x;
velocity = [gestureRecognizer velocityInView:gestureRecognizer.view].x;
distance = gestureRecognizer.view.bounds.size.width;
BOOL isRTL = _controller.view.semanticContentAttribute == UISemanticContentAttributeForceRightToLeft;
if (isRTL) {
translation = -translation;
velocity = -velocity;
}
}

float transitionProgress = (translation / distance);
Expand Down
32 changes: 24 additions & 8 deletions ios/RNSScreenStackAnimator.m
Original file line number Diff line number Diff line change
Expand Up @@ -190,14 +190,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 @@ -242,6 +242,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, options `fullScreenSwipeEnabled: true`, `customAnimationOnSwipe: true` and `stackAnimation: 'slide_from_bottom'` are 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 @@ -343,6 +343,15 @@ export type NativeStackNavigationOptions = {
* @platform android
*/
statusBarTranslucent?: boolean;
/**
* Sets the direction in which you should swipe to dismiss the screen.
* When using `vertical` option, options `fullScreenSwipeEnabled: true`, `customAnimationOnSwipe: true` and `stackAnimation: 'slide_from_bottom'` are 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,8 +149,6 @@ const RouteView = ({
}) => {
const { options, render: renderScene } = descriptors[route.key];
const {
customAnimationOnSwipe,
fullScreenSwipeEnabled,
gestureEnabled,
headerShown,
homeIndicatorHidden,
Expand All @@ -159,16 +157,38 @@ const RouteView = ({
navigationBarHidden,
replaceAnimation = 'pop',
screenOrientation,
stackAnimation,
statusBarAnimation,
statusBarColor,
statusBarHidden,
statusBarStyle,
statusBarTranslucent,
swipeDirection = 'horizontal',
transitionDuration,
} = options;

let { stackPresentation = 'push' } = options;
let {
customAnimationOnSwipe,
fullScreenSwipeEnabled,
stackAnimation,
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;
}
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 @@ -212,6 +232,7 @@ const RouteView = ({
statusBarHidden={statusBarHidden}
statusBarStyle={statusBarStyle}
statusBarTranslucent={statusBarTranslucent}
swipeDirection={swipeDirection}
transitionDuration={transitionDuration}
onHeaderBackButtonClicked={() => {
navigation.dispatch({
Expand Down
11 changes: 11 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 @@ -232,6 +233,16 @@ 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, options `fullScreenSwipeEnabled: true`, `customAnimationOnSwipe: true` and `stackAnimation: 'slide_from_bottom'` are set by default.
* The following values are supported:
* - `vertical` – dismiss screen vertically
* - `horizontal` – dismiss screen horizontally (default)
*
* @platform ios
*/
swipeDirection?: SwipeDirectionTypes;
/**
* Changes the duration (in milliseconds) of `slide_from_bottom`, `fade_from_bottom`, `fade` and `simple_push` transitions on iOS. Defaults to `350`.
* The duration of `default` and `flip` transitions isn't customizable.
Expand Down

0 comments on commit f2ac36b

Please sign in to comment.