Repository: enesozturk/react-native-ios
Branch: main
Commit: 89f73c56a514
Files: 53
Total size: 53.8 KB
Directory structure:
gitextract_g5jugsi7/
├── .expo-shared/
│ └── assets.json
├── .gitignore
├── .svgrrc
├── App.tsx
├── LICENSE
├── README.md
├── app.json
├── babel.config.aliases.js
├── babel.config.js
├── declarations.d.ts
├── metro.config.js
├── package.json
├── src/
│ ├── components/
│ │ ├── AnimatedInput/
│ │ │ ├── AnimatedInput.hooks.ts
│ │ │ ├── AnimatedInput.styles.ts
│ │ │ ├── AnimatedIntput.tsx
│ │ │ ├── CancelButton.tsx
│ │ │ └── index.ts
│ │ ├── AppItem/
│ │ │ ├── AppItem.constants.ts
│ │ │ ├── AppItem.styles.ts
│ │ │ ├── AppItem.tsx
│ │ │ └── index.ts
│ │ ├── Footer/
│ │ │ ├── Footer.tsx
│ │ │ └── index.ts
│ │ ├── SwipeableProvider/
│ │ │ ├── SwipeableProvider.styles.ts
│ │ │ ├── SwipeableProvider.tsx
│ │ │ └── index.ts
│ │ ├── Text.tsx
│ │ ├── Wallpaper.tsx
│ │ └── WidgetItem/
│ │ ├── WidgetItem.constants.ts
│ │ ├── WidgetItem.styles.ts
│ │ ├── WidgetItem.tsx
│ │ └── index.ts
│ ├── configs/
│ │ ├── horizontalGestureCalculations.ts
│ │ └── verticalGestureCalculations.ts
│ ├── constants/
│ │ ├── animation.ts
│ │ ├── apps.ts
│ │ ├── theme.ts
│ │ └── ui.ts
│ ├── hooks/
│ │ ├── useGestureHandler.ts
│ │ └── useSwipeableProvider.ts
│ ├── screens/
│ │ ├── Home/
│ │ │ ├── Home.tsx
│ │ │ └── index.ts
│ │ └── Search/
│ │ ├── AnimatedProvider.tsx
│ │ ├── LeftSearch.tsx
│ │ ├── RightSearch.tsx
│ │ ├── Search.styles.ts
│ │ ├── Search.tsx
│ │ ├── SearchContent.tsx
│ │ ├── components/
│ │ │ ├── LeftSearchContent.styles.tsx
│ │ │ └── LeftSearchContent.tsx
│ │ └── index.ts
│ └── utils/
│ └── swipeableGestureHandlers.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .expo-shared/assets.json
================================================
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}
================================================
FILE: .gitignore
================================================
node_modules/
.expo/
dist/
npm-debug.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
# macOS
.DS_Store
================================================
FILE: .svgrrc
================================================
{
"memo": true,
"native": true,
"replaceAttrValues": {
"fill": "{props.fill}",
"stroke": "{props.stroke}"
}
}
================================================
FILE: App.tsx
================================================
import { StyleSheet } from "react-native";
import { StatusBar } from "expo-status-bar";
import { SafeAreaProvider } from "react-native-safe-area-context";
import Wallpaper from "@react-native-ios/components/Wallpaper";
import Footer from "@react-native-ios/components/Footer/Footer";
import SwipeableProvider from "@react-native-ios/components/SwipeableProvider";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { Page1, Page2 } from "@react-native-ios/screens/Home";
export default function App() {
return (
, ]} />
);
}
const styles = StyleSheet.create({
gestureHandler: {
flex: 1,
},
});
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2022 Enes Ozturk
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
React Native iOS
This is a fun project. I have always been impressed with iOS's UI gestures & animations. Previously I developed [React Native Hold Menu](https://github.com/enesozturk/react-native-hold-menu) which I inspired by the iOS messages, applications hold to open menus. This time, I tried to do the whole iOS itself for fun, and the result is awesome!
## Stack
- [Expo](https://expo.dev/) (SDK 44)
- TypeScript
- [React Native Reanimated](https://docs.swmansion.com/react-native-reanimated/)
- [React Native Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/docs/)
- [Expo Blur](https://github.com/expo/expo/tree/master/packages/expo-blur) for animatable blur component
## Want to try?
If you want to try this on your device, you can install and run the app in a few seconds with the following commands;
Install the packages:
```
yarn install
```
Start the server
```bash
yarn start
```
You will see a QR code on the terminal, you can scan this QR code with the device which you are on the same network. Or you can start the simulator by pressing `i`, on the terminal.
> Currently app is not working the same with iOS on Android due to some issues with blur view animations and horizontal gesture animations. I may fix it later.
That's it, enjoy 🤞🏽
## License
The source code is made available under the [MIT license](./LICENSE).
## Show Your Support
If you like this project, please give a star and follow me on Github for more 🤩
================================================
FILE: app.json
================================================
{
"expo": {
"name": "react-native-ios",
"slug": "react-native-ios",
"privacy": "public",
"platforms": ["ios"],
"sdkVersion": "44.0.0",
"githubUrl": "https://github.com/enesozturk/react-native-ios",
"version": "1.0.2",
"orientation": "portrait",
"owner": "enesozturk",
"icon": "./assets/icon.png",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "cover",
"backgroundColor": "#000000"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.enesozturk.react-native-ios"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF"
}
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
================================================
FILE: babel.config.aliases.js
================================================
module.exports = {
"@react-native-ios/api": "./src/api",
"@react-native-ios/assets": "./src/assets",
"@react-native-ios/components": "./src/components",
"@react-native-ios/configs": "./src/configs",
"@react-native-ios/constants": "./src/constants",
"@react-native-ios/helpers": "./src/helpers",
"@react-native-ios/hooks": "./src/hooks",
"@react-native-ios/navigators": "./src/navigators",
"@react-native-ios/providers": "./src/providers",
"@react-native-ios/screens": "./src/screens",
"@react-native-ios/store": "./src/store",
"@react-native-ios/storybook": "./src/storybook",
"@react-native-ios/translations": "./src/translations",
"@react-native-ios/utils": "./src/utils",
};
================================================
FILE: babel.config.js
================================================
const aliases = require("./babel.config.aliases");
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [
["module-resolver", { root: ["./"], alias: aliases }],
"react-native-reanimated/plugin",
],
};
};
================================================
FILE: declarations.d.ts
================================================
declare module "*.svg" {
import React from "react";
import { SvgProps } from "react-native-svg";
const content: React.FC;
export default content;
}
declare module "*.jpg";
================================================
FILE: metro.config.js
================================================
const { getDefaultConfig } = require("expo/metro-config");
module.exports = (async () => {
const {
resolver: { sourceExts, assetExts },
} = await getDefaultConfig(__dirname);
return {
...getDefaultConfig,
transformer: {
babelTransformerPath: require.resolve("react-native-svg-transformer"),
getTransformOptions: async () => ({
transform: {
inlineRequires: true,
},
}),
},
resolver: {
assetExts: assetExts.filter((ext) => ext !== "svg"),
sourceExts: [...sourceExts, "svg"],
},
};
})();
================================================
FILE: package.json
================================================
{
"name": "react-native-ios",
"version": "1.0.0",
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject"
},
"dependencies": {
"expo": "~44.0.0",
"expo-blur": "~11.0.0",
"expo-status-bar": "~1.2.0",
"expo-web-browser": "~10.1.0",
"react": "17.0.1",
"react-dom": "17.0.1",
"react-native": "0.64.3",
"react-native-gesture-handler": "~2.1.0",
"react-native-reanimated": "~2.3.1",
"react-native-safe-area-context": "3.3.2",
"react-native-svg": "^12.1.1",
"react-native-web": "0.17.1"
},
"devDependencies": {
"@babel/core": "^7.12.9",
"@svgr/cli": "^6.2.0",
"@types/react": "~17.0.21",
"@types/react-native": "~0.64.12",
"babel-plugin-module-resolver": "^4.1.0",
"react-native-svg-transformer": "^1.0.0",
"typescript": "~4.3.5"
},
"private": true
}
================================================
FILE: src/components/AnimatedInput/AnimatedInput.hooks.ts
================================================
import {
Extrapolate,
interpolate,
useAnimatedStyle,
useSharedValue,
} from "react-native-reanimated";
import theme from "@react-native-ios/constants/theme";
import { SCREEN_WIDTH } from "@react-native-ios/constants/ui";
const INPUT_WIDTH = SCREEN_WIDTH - 54;
const useAnimatedInput = () => {
const containerWidth = useSharedValue(0);
const cancelTextWidth = useSharedValue(0);
const active = useSharedValue(0);
const animatedContainerStyles = useAnimatedStyle(
() => ({
width: "100%",
}),
[]
);
const animatedContentContainerStyles = useAnimatedStyle(
() => ({
height: interpolate(
active.value,
[0, 1],
[22 + 26, 22 + 12], // 48 - 24 24/2 14, 38-20 18/2 7
Extrapolate.CLAMP
),
width: interpolate(
active.value,
[0, 1],
[INPUT_WIDTH, INPUT_WIDTH - cancelTextWidth.value],
Extrapolate.CLAMP
),
borderRadius: interpolate(
active.value,
[0, 1],
[theme.spacing.md, 12],
Extrapolate.CLAMP
),
}),
[cancelTextWidth]
);
const animatedInputContainerStyles = useAnimatedStyle(
() => ({
position: "absolute",
left: interpolate(
active.value,
[0, 1],
[containerWidth.value / 2, 0],
Extrapolate.CLAMP
),
}),
[]
);
const animatedInputStyles = useAnimatedStyle(
() => ({
marginLeft: interpolate(
active.value,
[0, 1],
[INPUT_WIDTH / 2 - 30, (theme.spacing.sm + theme.spacing.xs) * 2 - 2],
Extrapolate.CLAMP
),
marginBottom: interpolate(
active.value,
[0, 1],
[4, 2],
Extrapolate.CLAMP
),
transform: [
{
scale: interpolate(
active.value,
[0, 1],
[1, 0.84],
Extrapolate.CLAMP
),
},
],
}),
[]
);
const animatedCancelTextStyles = useAnimatedStyle(
() => ({
opacity: active.value,
}),
[]
);
const animatedSearchIconStyles = useAnimatedStyle(
() => ({
opacity: 1,
top: interpolate(active.value, [0, 1], [12, 8], Extrapolate.CLAMP),
left: interpolate(
active.value,
[0, 1],
[INPUT_WIDTH / 2 - 30 - 32, 8],
Extrapolate.CLAMP
),
}),
[]
);
const animatedSearchIconProps = useAnimatedStyle(
() => ({
width: interpolate(active.value, [0, 1], [20, 18], Extrapolate.CLAMP),
height: interpolate(active.value, [0, 1], [20, 18], Extrapolate.CLAMP),
}),
[]
);
const animatedMicIconStyles = useAnimatedStyle(
() => ({
opacity: active.value,
top: interpolate(active.value, [0, 1], [14, 8], Extrapolate.CLAMP),
}),
[]
);
return {
active,
cancelTextWidth,
containerWidth,
animatedSearchIconStyles,
animatedSearchIconProps,
animatedMicIconStyles,
animatedContainerStyles,
animatedContentContainerStyles,
animatedInputContainerStyles,
animatedInputStyles,
animatedCancelTextStyles,
};
};
export default useAnimatedInput;
================================================
FILE: src/components/AnimatedInput/AnimatedInput.styles.ts
================================================
import { StyleSheet } from "react-native";
import theme from "@react-native-ios/constants/theme";
import { SCREEN_WIDTH } from "@react-native-ios/constants/ui";
export default StyleSheet.create({
container: {
display: "flex",
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
marginBottom: theme.spacing.lg,
paddingHorizontal: 56 / 2 - 8,
},
contentContainer: {
width: SCREEN_WIDTH - theme.spacing.lg,
display: "flex",
flexDirection: "row",
position: "relative",
backgroundColor: theme.colors.white.white25,
},
input: {
paddingHorizontal: theme.spacing.sm,
paddingRight: 36,
color: theme.colors.white.white75,
...theme.font.title2,
},
searchIconContainer: {
position: "absolute",
},
michrophoneIconContainer: {
position: "absolute",
right: 0,
},
cancelText: {
...theme.font.body,
textAlignVertical: "center",
color: theme.colors.white.white50,
height: "100%",
paddingVertical: theme.spacing.sm13,
},
});
================================================
FILE: src/components/AnimatedInput/AnimatedIntput.tsx
================================================
import React from "react";
import { TextInput, View } from "react-native";
import Animated, { withTiming } from "react-native-reanimated";
import { TapGestureHandler } from "react-native-gesture-handler";
import theme from "@react-native-ios/constants/theme";
import SearchSVG from "@react-native-ios/assets/svg/search.svg";
import MichrophoneSVG from "@react-native-ios/assets/svg/michrophone.svg";
import styles from "./AnimatedInput.styles";
import useAnimatedInput from "./AnimatedInput.hooks";
import { useRef } from "react";
import CancelButton from "./CancelButton";
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
const AnimatedSearchIcon = Animated.createAnimatedComponent(SearchSVG);
export default function AnimatedIntput() {
const {
active,
cancelTextWidth,
containerWidth,
animatedSearchIconStyles,
animatedMicIconStyles,
animatedSearchIconProps,
animatedContainerStyles,
animatedContentContainerStyles,
animatedInputStyles,
animatedCancelTextStyles,
} = useAnimatedInput();
const inputRef = useRef(null);
return (
{}}>
{
inputRef?.current?.focus();
active.value = withTiming(1, { duration: 300 });
}}
>
{
containerWidth.value = e.nativeEvent.layout.width;
}}
style={[styles.contentContainer, animatedContentContainerStyles]}
>
{
active.value = withTiming(1, { duration: 300 });
}}
onBlur={() => {
active.value = withTiming(0, { duration: 300 });
}}
/>
{
inputRef?.current?.blur();
}}
cancelTextWidth={cancelTextWidth}
/>
);
}
================================================
FILE: src/components/AnimatedInput/CancelButton.tsx
================================================
import React from "react";
import { StyleSheet, View } from "react-native";
import { TapGestureHandler } from "react-native-gesture-handler";
import Animated, { SharedValue, withSpring } from "react-native-reanimated";
import { SPRING_CONFIG } from "@react-native-ios/constants/animation";
import theme from "@react-native-ios/constants/theme";
type CancelButtonProps = {
active: SharedValue;
cancelTextWidth: SharedValue;
animatedCancelTextStyles: any;
onClose: () => void;
};
export default function CancelButton({
active,
cancelTextWidth,
animatedCancelTextStyles,
onClose,
}: CancelButtonProps) {
return (
{
cancelTextWidth.value = e.nativeEvent.layout.width;
}}
>
{
onClose?.();
active.value = withSpring(0, SPRING_CONFIG);
}}
>
Cancel
);
}
const styles = StyleSheet.create({
container: {
paddingVertical: theme.spacing.sm13,
display: "flex",
flexDirection: "row",
},
cancelText: {
...theme.font.body,
color: theme.colors.white.white50,
height: "100%",
marginLeft: 4,
},
});
================================================
FILE: src/components/AnimatedInput/index.ts
================================================
export { default } from "./AnimatedIntput";
================================================
FILE: src/components/AppItem/AppItem.constants.ts
================================================
import { SCREEN_WIDTH } from "@react-native-ios/constants/ui";
export const APP_ICON_WIDTH_RATIO = 828 / 128;
export const APP_ICON_SIZE = SCREEN_WIDTH / APP_ICON_WIDTH_RATIO;
================================================
FILE: src/components/AppItem/AppItem.styles.ts
================================================
import { StyleSheet } from "react-native";
import theme from "@react-native-ios/constants/theme";
import { APP_ICON_SIZE } from "./AppItem.constants";
export default StyleSheet.create({
container: {},
image: {
width: "100%",
height: "100%",
borderRadius: 17,
},
title: {
position: "absolute",
bottom: -20,
left: -8,
textAlign: "center",
color: "white",
width: APP_ICON_SIZE + 16,
...theme.font.caption1,
},
});
================================================
FILE: src/components/AppItem/AppItem.tsx
================================================
import { Image, Text, View } from "react-native";
import { APP_ICON_SIZE } from "./AppItem.constants";
import styles from "./AppItem.styles";
type AppItemProps = {
size?: "small" | undefined;
noTitle?: boolean;
title?: string;
icon: any;
};
export default function AppItem({ icon, title, size, noTitle }: AppItemProps) {
return (
{noTitle || size === "small" ? null : (
{title ? title : "Square"}
)}
);
}
================================================
FILE: src/components/AppItem/index.ts
================================================
export { default } from "./AppItem";
================================================
FILE: src/components/Footer/Footer.tsx
================================================
import { StyleSheet, View } from "react-native";
import { BlurView } from "expo-blur";
import AppItem from "@react-native-ios/components/AppItem";
import apps from "@react-native-ios/constants/apps";
import { SCREEN_WIDTH } from "@react-native-ios/constants/ui";
export default function Footer() {
return (
{Object.keys(apps.footer).map((item, index) => {
return (
);
})}
);
}
const styles = StyleSheet.create({
container: {
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
},
contentContainer: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
paddingVertical: 20,
paddingHorizontal: 20,
position: "absolute",
bottom: 24,
width: SCREEN_WIDTH - 24,
borderRadius: 36,
overflow: "hidden",
},
});
================================================
FILE: src/components/Footer/index.ts
================================================
export { default } from "./Footer";
================================================
FILE: src/components/SwipeableProvider/SwipeableProvider.styles.ts
================================================
import { StyleSheet } from "react-native";
import { SCREEN_HEIGHT, SCREEN_WIDTH } from "@react-native-ios/constants/ui";
export default StyleSheet.create({
pagesContainer: {
width: SCREEN_WIDTH * 2,
height: SCREEN_HEIGHT,
display: "flex",
flexDirection: "row",
},
pageContainer: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
display: "flex",
},
container: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
},
searchContainer: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
...StyleSheet.absoluteFillObject,
},
});
================================================
FILE: src/components/SwipeableProvider/SwipeableProvider.tsx
================================================
import React from "react";
import { GestureDetector } from "react-native-gesture-handler";
import Animated from "react-native-reanimated";
import Search from "@react-native-ios/screens/Search";
import RightSearch from "@react-native-ios/screens/Search/RightSearch";
import LeftSearch from "@react-native-ios/screens/Search/LeftSearch";
import useSwipeableProvider from "@react-native-ios/hooks/useSwipeableProvider";
import styles from "./SwipeableProvider.styles";
type SwipeableProviderProps = {
pages: React.ReactNode[];
};
export default function SwipeableProvider({ pages }: SwipeableProviderProps) {
const {
offsetY,
offsetX,
startX,
animatedStyles,
animatedPageContainerStyles,
animatedPagesContainerStyles,
swipeableProviderGesture,
} = useSwipeableProvider();
return (
<>
{pages.map((item) => (
{item}
))}
>
);
}
================================================
FILE: src/components/SwipeableProvider/index.ts
================================================
export { default } from "./SwipeableProvider";
================================================
FILE: src/components/Text.tsx
================================================
import React from "react";
import { Text as RNText, TextProps } from "react-native";
export default function Text(props: TextProps) {
return (
{props.children}
);
}
================================================
FILE: src/components/Wallpaper.tsx
================================================
import { useLayoutEffect, useState } from "react";
import { Image, StyleSheet, View } from "react-native";
import { SCREEN_WIDTH } from "../constants/ui";
import WalpaperImage from "../assets/img/wallpaper.jpg";
export default function Wallpaper() {
const [imageWidth, setImageWidth] = useState(0);
const [imageHeight, setImageHeight] = useState(0);
useLayoutEffect(() => {
const { width, height } = Image.resolveAssetSource(WalpaperImage);
setImageWidth(SCREEN_WIDTH);
setImageHeight((SCREEN_WIDTH * height) / width);
}, []);
return (
);
}
================================================
FILE: src/components/WidgetItem/WidgetItem.constants.ts
================================================
import theme from "@react-native-ios/constants/theme";
import { SCREEN_WIDTH } from "@react-native-ios/constants/ui";
export const WIDGET_SQUARE_SIZE =
(SCREEN_WIDTH - (theme.spacing.lg + 4) * 2 - 21) / 2;
export const WIDGET_WIDE_SIZE = SCREEN_WIDTH - theme.spacing.lg * 2;
================================================
FILE: src/components/WidgetItem/WidgetItem.styles.ts
================================================
import { StyleSheet } from "react-native";
import theme from "@react-native-ios/constants/theme";
import { WIDGET_SQUARE_SIZE } from "./WidgetItem.constants";
export default StyleSheet.create({
container: {
width: WIDGET_SQUARE_SIZE,
height: WIDGET_SQUARE_SIZE,
borderRadius: theme.spacing.lg,
backgroundColor: theme.colors.white.white25,
overflow: "hidden",
marginBottom: theme.spacing.lg,
},
});
================================================
FILE: src/components/WidgetItem/WidgetItem.tsx
================================================
import React from "react";
import { View } from "react-native";
import { BlurView } from "expo-blur";
import { TapGestureHandler } from "react-native-gesture-handler";
import { WIDGET_SQUARE_SIZE } from "./WidgetItem.constants";
import styles from "./WidgetItem.styles";
type WidgetItemProps = {
children?: React.ReactNode;
containerStyles?: object;
onPress?: () => void;
wide?: boolean;
};
export default function WidgetItem({
children,
containerStyles,
wide,
onPress,
}: WidgetItemProps) {
return (
{children}
);
}
================================================
FILE: src/components/WidgetItem/index.ts
================================================
export { default } from "./WidgetItem";
================================================
FILE: src/configs/horizontalGestureCalculations.ts
================================================
import { SharedValue, withSpring } from "react-native-reanimated";
import { PanGestureHandlerEventPayload } from "react-native-gesture-handler";
import {
MIN_VELOCITY_Y_TO_ACTIVATE,
SNAP_POINTS_HORIZONTAL_AS_ARRAY,
SPRING_CONFIG,
} from "@react-native-ios/constants/animation";
import { SCREEN_WIDTH } from "@react-native-ios/constants/ui";
type handleOnUpdateHorizontalProps = {
e: PanGestureHandlerEventPayload;
startX: SharedValue;
offsetX: SharedValue;
};
const getNextSnapPoint = (offset: number) => {
"worklet";
let nextSnapPointAvailable = false;
let snapPoint = 0;
for (let i = 2; i < 10; i += 2) {
if (
offset <= SNAP_POINTS_HORIZONTAL_AS_ARRAY[i - 1] &&
offset >= SNAP_POINTS_HORIZONTAL_AS_ARRAY[i + 1]
) {
snapPoint = SNAP_POINTS_HORIZONTAL_AS_ARRAY[i];
nextSnapPointAvailable = true;
}
}
return nextSnapPointAvailable ? snapPoint : -1;
};
export const handleOnEndHorizontal = ({
e,
startX,
offsetX,
}: handleOnUpdateHorizontalProps & {
destination: SharedValue;
}) => {
"worklet";
const velocity = Math.abs(e.velocityX);
const direction = e.translationX < 0 ? "right" : "left";
const nextXValue =
direction === "right"
? offsetX.value - SCREEN_WIDTH
: offsetX.value + SCREEN_WIDTH;
startX.value = startX.value + e.translationX;
if (velocity > MIN_VELOCITY_Y_TO_ACTIVATE) {
const nextSnapPoint = getNextSnapPoint(nextXValue);
if (nextSnapPoint !== -1) {
offsetX.value = withSpring(nextSnapPoint, {
...SPRING_CONFIG,
velocity,
});
startX.value = withSpring(nextSnapPoint, {
...SPRING_CONFIG,
velocity,
});
}
} else {
const nextSnapPoint2 = getNextSnapPoint(offsetX.value + e.translationX);
console.log("else", nextSnapPoint2);
offsetX.value = withSpring(nextSnapPoint2, {
...SPRING_CONFIG,
velocity,
});
startX.value = withSpring(nextSnapPoint2, {
...SPRING_CONFIG,
velocity,
});
}
};
================================================
FILE: src/configs/verticalGestureCalculations.ts
================================================
import { SharedValue, withSpring } from "react-native-reanimated";
import { PanGestureHandlerEventPayload } from "react-native-gesture-handler";
import {
DISTANCE_TO_ACTIVATE,
MAX_OFFSET_TO_ANIMATE,
MIN_VELOCITY_Y_TO_ACTIVATE,
SPRING_CONFIG,
} from "@react-native-ios/constants/animation";
type handleOnUpdateVerticalProps = {
e: PanGestureHandlerEventPayload;
offsetY: SharedValue;
};
export const handleOnEndVertical = ({
e,
offsetY,
}: handleOnUpdateVerticalProps) => {
"worklet";
const velocity = Math.abs(e.velocityY);
const translation = Math.abs(e.translationY);
if (
translation > DISTANCE_TO_ACTIVATE ||
velocity > MIN_VELOCITY_Y_TO_ACTIVATE
) {
offsetY.value = withSpring(MAX_OFFSET_TO_ANIMATE, {
...SPRING_CONFIG,
overshootClamping: false,
velocity,
});
} else {
offsetY.value = withSpring(0, SPRING_CONFIG);
}
};
================================================
FILE: src/constants/animation.ts
================================================
import { SCREEN_WIDTH } from "./ui";
export const BLUR_VIEW_MAX_INTENSITY = 75;
export const MAX_OFFSET_TO_ANIMATE = 90;
export const DISTANCE_TO_ACTIVATE = MAX_OFFSET_TO_ANIMATE / 2;
export const MIN_VELOCITY_Y_TO_ACTIVATE = 100;
export const SPRING_CONFIG = {
damping: 500,
stiffness: 1000,
mass: 3,
overshootClamping: true,
restDisplacementThreshold: 10,
restSpeedThreshold: 10,
};
export const SNAP_POINTS_HORIZONTAL = {
LEFT_PAGE: SCREEN_WIDTH,
LEFT_PAGE_HALF: SCREEN_WIDTH / 2,
ORIGIN: 0,
FIRST_PAGE_HALF: SCREEN_WIDTH / -2,
SECOND_PAGE: SCREEN_WIDTH * -1,
SECOND_PAGE_HALF: (SCREEN_WIDTH * -3) / 2,
RIGHT_PAGE: SCREEN_WIDTH * -2, // -750
};
export const SNAP_POINTS_HORIZONTAL_AS_ARRAY = [
SCREEN_WIDTH * 3,
(SCREEN_WIDTH * 3) / 2,
SCREEN_WIDTH,
SCREEN_WIDTH / 2, // 187.5
0,
SCREEN_WIDTH / -2, // -187
SCREEN_WIDTH * -1, // -375
(SCREEN_WIDTH * -3) / 2, // -375 + (-375/2) = -562
SCREEN_WIDTH * -2, // -375 * 2 == -750
(SCREEN_WIDTH * -5) / 2, // -375 * 2 + (-375/2) = -937
SCREEN_WIDTH * -3, // -375 * 3 = -1500
(SCREEN_WIDTH * -7) / 2, // -375 * 3 + (-375/2) = -1875
];
================================================
FILE: src/constants/apps.ts
================================================
export default {
footer: {
settings: {
title: "Settings",
icon: require("../assets/img/app-icons/Settings.png"),
},
safari: {
title: "Safari",
icon: require("../assets/img/app-icons/Safari.png"),
},
photos: {
title: "Photos",
icon: require("../assets/img/app-icons/Photos.png"),
},
messages: {
title: "Messages",
icon: require("../assets/img/app-icons/Messages.png"),
},
},
home: {
row1: [
{
title: "FaceTime",
icon: require("../assets/img/app-icons/Facetime.png"),
},
{
title: "Messages",
icon: require("../assets/img/app-icons/Messages.png"),
},
{
title: "Clock",
icon: require("../assets/img/app-icons/Clock.png"),
},
{
title: "Weather",
icon: require("../assets/img/app-icons/Weather.png"),
},
],
row2: [
{
title: "Notes",
icon: require("../assets/img/app-icons/Notes.png"),
},
{
title: "Calculator",
icon: require("../assets/img/app-icons/Calculator.png"),
},
{
title: "Wallet",
icon: require("../assets/img/app-icons/Wallet.png"),
},
{
title: "News",
icon: require("../assets/img/app-icons/News.png"),
},
],
row3: [
{
title: "Compass",
icon: require("../assets/img/app-icons/Compass.png"),
},
{
title: "iTunes",
icon: require("../assets/img/app-icons/iTunes.png"),
},
{
title: "AppStore",
icon: require("../assets/img/app-icons/AppStore.png"),
},
{
title: "Home",
icon: require("../assets/img/app-icons/Home.png"),
},
],
},
home2: {
row1: [],
},
};
================================================
FILE: src/constants/theme.ts
================================================
export default {
colors: {
white: {
white100: "rgba(255, 255, 255, 1)",
white75: "rgba(255, 255, 255, 0.75)",
white50: "rgba(255, 255, 255, 0.5)",
white25: "rgba(255, 255, 255, 0.25)",
white15: "rgba(255, 255, 255, 0.15)",
},
black: {
black100: "rgba(0, 0, 0, 1)",
black75: "rgba(0, 0, 0, 0.75)",
black50: "rgba(0, 0, 0, 0.5)",
black25: "rgba(0, 0, 0, 0.25)",
},
},
spacing: {
xs: 4,
sm: 7,
sm13: 13,
md: 16,
lg: 24,
xl: 32,
},
font: {
largeTitle: { fontSize: 34, lineHeight: 41 },
title1: { fontSize: 28, lineHeight: 34 },
title2: { fontSize: 22, lineHeight: 28 },
title3: { fontSize: 20, lineHeight: 25 },
headline: { fontSize: 17, lineHeight: 22 },
body: { fontSize: 17, lineHeight: 22 },
callout: { fontSize: 16, lineHeight: 21 },
subhead: { fontSize: 15, lineHeight: 20 },
footnote: { fontSize: 13, lineHeight: 18 },
caption1: { fontSize: 12, lineHeight: 16 },
caption2: { fontSize: 11, lineHeight: 13 },
},
};
================================================
FILE: src/constants/ui.ts
================================================
import { Dimensions } from "react-native";
const { width, height } = Dimensions.get("screen");
export const SCREEN_WIDTH = width;
export const SCREEN_HEIGHT = height;
================================================
FILE: src/hooks/useGestureHandler.ts
================================================
import {
handleGestureOnEnd,
handleGestureOnStart,
handleGestureOnUpdate,
} from "@react-native-ios/utils/swipeableGestureHandlers";
import { Gesture } from "react-native-gesture-handler";
import { SharedValue, useSharedValue } from "react-native-reanimated";
type useGestureHandlerProps = {
startX: SharedValue;
offsetX: SharedValue;
offsetY?: SharedValue;
};
const useGestureHandler = ({
startX,
offsetX,
offsetY,
}: useGestureHandlerProps) => {
const direction = useSharedValue(0);
const destination = useSharedValue(0);
const swipeableProviderGesture = Gesture.Pan()
.onStart((e) => {
handleGestureOnStart({
direction,
e,
startX,
});
})
.onUpdate((e) => {
handleGestureOnUpdate({
direction,
e,
startX,
offsetX,
offsetY,
});
})
.onEnd((e) => {
handleGestureOnEnd({
direction,
e,
startX,
offsetX,
offsetY,
destination,
});
});
return { swipeableProviderGesture };
};
export default useGestureHandler;
================================================
FILE: src/hooks/useSwipeableProvider.ts
================================================
import {
Extrapolate,
interpolate,
useAnimatedStyle,
useSharedValue,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
MAX_OFFSET_TO_ANIMATE,
SNAP_POINTS_HORIZONTAL,
} from "@react-native-ios/constants/animation";
import theme from "@react-native-ios/constants/theme";
import useGestureHandler from "./useGestureHandler";
const useSwipeableProvider = () => {
const { top } = useSafeAreaInsets();
const offsetY = useSharedValue(0);
const offsetX = useSharedValue(0);
const startX = useSharedValue(0);
const { swipeableProviderGesture } = useGestureHandler({
offsetY,
offsetX,
startX,
});
const animatedStyles = useAnimatedStyle(() => {
return {
transform: [{ translateY: offsetY.value }],
opacity: interpolate(
offsetY.value,
[0, MAX_OFFSET_TO_ANIMATE],
[1, 0.6],
Extrapolate.CLAMP
),
paddingTop: top + theme.spacing.md,
};
});
const animatedPageContainerStyles = useAnimatedStyle(() => {
return {
transform: [
{
scale:
offsetX.value > 0
? interpolate(
offsetX.value,
[
SNAP_POINTS_HORIZONTAL.LEFT_PAGE,
SNAP_POINTS_HORIZONTAL.ORIGIN,
],
[0.85, 1],
Extrapolate.CLAMP
)
: interpolate(
offsetX.value,
[
SNAP_POINTS_HORIZONTAL.SECOND_PAGE,
SNAP_POINTS_HORIZONTAL.RIGHT_PAGE,
],
[1, 0.85],
Extrapolate.CLAMP
),
},
],
};
});
const animatedPagesContainerStyles = useAnimatedStyle(() => {
return {
transform: [
{
translateX: Math.min(
Math.max(offsetX.value, SNAP_POINTS_HORIZONTAL.SECOND_PAGE),
SNAP_POINTS_HORIZONTAL.ORIGIN
),
},
],
};
}, [offsetX]);
return {
offsetY,
offsetX,
startX,
animatedStyles,
animatedPageContainerStyles,
animatedPagesContainerStyles,
swipeableProviderGesture,
};
};
export default useSwipeableProvider;
================================================
FILE: src/screens/Home/Home.tsx
================================================
import { StyleSheet, View } from "react-native";
import { SCREEN_HEIGHT, SCREEN_WIDTH } from "@react-native-ios/constants/ui";
import AppItem from "@react-native-ios/components/AppItem";
import apps from "@react-native-ios/constants/apps";
export function Page1() {
return (
{apps.home.row1.map((item) => {
return ;
})}
{apps.home.row2.map((item) => {
return ;
})}
{apps.home.row3.map((item) => {
return ;
})}
);
}
export function Page2() {
return (
{apps.home.row3.map((item) => {
return ;
})}
{apps.home.row2.map((item) => {
return ;
})}
{apps.home.row1.map((item) => {
return ;
})}
);
}
const styles = StyleSheet.create({
gestureContainer: {
width: SCREEN_WIDTH * 2,
height: SCREEN_HEIGHT,
display: "flex",
flexDirection: "row",
},
container: {
flex: 1,
width: SCREEN_WIDTH,
display: "flex",
flexDirection: "column",
},
row: {
display: "flex",
flexDirection: "row",
width: "100%",
justifyContent: "space-between",
paddingHorizontal: 24,
marginBottom: 32,
},
});
================================================
FILE: src/screens/Home/index.ts
================================================
export { Page1, Page2 } from "./Home";
================================================
FILE: src/screens/Search/AnimatedProvider.tsx
================================================
import React from "react";
import { Keyboard, StyleSheet, View } from "react-native";
import { BlurView } from "expo-blur";
import { TapGestureHandler } from "react-native-gesture-handler";
import Animated, {
Extrapolate,
interpolate,
SharedValue,
useAnimatedProps,
useAnimatedStyle,
useSharedValue,
withSpring,
} from "react-native-reanimated";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
import {
BLUR_VIEW_MAX_INTENSITY,
MAX_OFFSET_TO_ANIMATE,
SNAP_POINTS_HORIZONTAL,
SPRING_CONFIG,
} from "@react-native-ios/constants/animation";
import { SCREEN_HEIGHT, SCREEN_WIDTH } from "@react-native-ios/constants/ui";
import theme from "@react-native-ios/constants/theme";
type AnimatedProviderProps = {
startPoint: number;
snapPoint: number;
direction: "left" | "right" | "vertical";
offset: SharedValue;
start?: SharedValue;
children: React.ReactNode;
};
export default function AnimatedProvider({
startPoint = 0,
snapPoint = 0,
direction,
offset,
start,
children,
}: AnimatedProviderProps) {
const { top } = useSafeAreaInsets();
const direMultiply = useSharedValue(direction == "right" ? -1 : 1);
const animatedBlurBackdropStyles = useAnimatedStyle(() => {
return {
zIndex:
offset.value * direMultiply.value > startPoint * direMultiply.value
? 10
: -1,
};
});
const animatedBlurBackdropProps = useAnimatedProps(() => {
return {
intensity: interpolate(
offset.value * direMultiply.value,
[startPoint * direMultiply.value, snapPoint * direMultiply.value],
[0, BLUR_VIEW_MAX_INTENSITY],
Extrapolate.CLAMP
),
};
});
const animatedContentStyles = useAnimatedStyle(
() => ({
opacity:
direction == "vertical"
? interpolate(
offset.value * direMultiply.value,
[startPoint * direMultiply.value, snapPoint * direMultiply.value],
[0, 1],
Extrapolate.CLAMP
)
: 1,
transform:
direction === "vertical"
? [{ translateY: offset.value - MAX_OFFSET_TO_ANIMATE }]
: [
{
translateX: interpolate(
offset.value * direMultiply.value,
[
startPoint * direMultiply.value,
snapPoint * direMultiply.value,
],
[SCREEN_WIDTH * (direction === "right" ? 1 : -1), 0],
Extrapolate.CLAMP
),
},
],
}),
[offset]
);
const handleTapOutside = () => {
offset.value = withSpring(startPoint, SPRING_CONFIG);
if (start) start.value = withSpring(startPoint, SPRING_CONFIG);
Keyboard.dismiss();
};
return (
{children}
);
}
const styles = StyleSheet.create({
container: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
},
contentContainer: {
display: "flex",
flexDirection: "column",
paddingHorizontal: theme.spacing.sm,
},
blurBackdrop: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
...StyleSheet.absoluteFillObject,
},
});
================================================
FILE: src/screens/Search/LeftSearch.tsx
================================================
import React from "react";
import { SharedValue } from "react-native-reanimated";
import { SNAP_POINTS_HORIZONTAL } from "@react-native-ios/constants/animation";
import LeftSearchContent from "./components/LeftSearchContent";
import AnimatedProvider from "./AnimatedProvider";
type AnimatedProviderProps = {
offsetX: SharedValue;
startX: SharedValue;
};
export default function LeftSearch({ offsetX, startX }: AnimatedProviderProps) {
return (
);
}
================================================
FILE: src/screens/Search/RightSearch.tsx
================================================
import React from "react";
import { SharedValue } from "react-native-reanimated";
import { SNAP_POINTS_HORIZONTAL } from "@react-native-ios/constants/animation";
import LeftSearchContent from "./components/LeftSearchContent";
import AnimatedProvider from "./AnimatedProvider";
type AnimatedProviderProps = {
offsetX: SharedValue;
startX: SharedValue;
};
export default function LeftSearch({ offsetX, startX }: AnimatedProviderProps) {
return (
);
}
================================================
FILE: src/screens/Search/Search.styles.ts
================================================
import { StyleSheet } from "react-native";
import theme from "@react-native-ios/constants/theme";
export default StyleSheet.create({
searchContainer: {
display: "flex",
flexDirection: "row",
alignItems: "center",
marginBottom: theme.spacing.md,
},
searchInputContainer: {
flex: 1,
},
searchIconContainer: {
position: "absolute",
top: theme.spacing.sm + theme.spacing.xs - 1,
left: theme.spacing.sm + theme.spacing.xs,
},
michrophoneIconContainer: {
position: "absolute",
top: 8,
right: 12 + theme.spacing.sm,
},
searchInput: {
paddingVertical: theme.spacing.sm,
paddingLeft: (theme.spacing.sm + theme.spacing.xs) * 2 + 14,
paddingRight: theme.spacing.sm * 2 + theme.spacing.xs + 12,
backgroundColor: theme.colors.white.white15,
color: theme.colors.white.white75,
borderRadius: 12,
marginRight: 8,
...theme.font.body,
},
cancelText: {
color: theme.colors.white.white50,
...theme.font.body,
},
titleSectionContainer: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: theme.spacing.xs,
marginBottom: theme.spacing.xs,
},
titleText: {
...theme.font.title2,
fontWeight: "600",
color: "white",
},
showMoreText: {
...theme.font.footnote,
color: theme.colors.white.white50,
},
appsContainer: {
display: "flex",
flexDirection: "column",
padding: theme.spacing.md,
borderRadius: theme.spacing.md,
width: "100%",
backgroundColor: theme.colors.white.white15,
},
row: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
marginBottom: theme.spacing.xl,
},
});
================================================
FILE: src/screens/Search/Search.tsx
================================================
import { MAX_OFFSET_TO_ANIMATE } from "@react-native-ios/constants/animation";
import React from "react";
import { SharedValue } from "react-native-reanimated";
import AnimatedProvider from "./AnimatedProvider";
import SearchContent from "./SearchContent";
type SearchProps = {
offsetY: SharedValue;
};
export default function Search({ offsetY }: SearchProps) {
return (
);
}
================================================
FILE: src/screens/Search/SearchContent.tsx
================================================
import React from "react";
import { Pressable, Text, View } from "react-native";
import { TextInput } from "react-native-gesture-handler";
import SearchSVG from "@react-native-ios/assets/svg/search.svg";
import MichrophoneSVG from "@react-native-ios/assets/svg/michrophone.svg";
import ChevronRightSVG from "@react-native-ios/assets/svg/chevron-right.svg";
import AppItem from "@react-native-ios/components/AppItem";
import apps from "@react-native-ios/constants/apps";
import styles from "./Search.styles";
import theme from "@react-native-ios/constants/theme";
export default function SearchContent() {
return (
<>
Cancel
Siri Suggestions
{apps.home.row3.map((item) => {
return ;
})}
{apps.home.row1.map((item) => {
return ;
})}
>
);
}
================================================
FILE: src/screens/Search/components/LeftSearchContent.styles.tsx
================================================
import { StyleSheet } from "react-native";
import theme from "@react-native-ios/constants/theme";
export default StyleSheet.create({
searchContainer: {
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: theme.spacing.lg - theme.spacing.sm,
},
searchInputContainer: {
flex: 1,
paddingVertical: theme.spacing.md,
borderRadius: theme.spacing.md,
backgroundColor: theme.colors.white.white15,
display: "flex",
flexDirection: "row",
justifyContent: "center",
},
searchIconContainer: {
position: "absolute",
top: theme.spacing.sm + theme.spacing.xs - 1,
left: theme.spacing.sm + theme.spacing.xs,
},
michrophoneIconContainer: {
position: "absolute",
top: theme.spacing.sm,
right: 12 + theme.spacing.sm,
},
searchInput: {
paddingHorizontal: theme.spacing.sm,
color: theme.colors.white.white75,
...theme.font.body,
},
cancelButton: {
position: "absolute",
right: 0,
opacity: 0,
},
cancelText: {
color: theme.colors.white.white50,
...theme.font.body,
},
appsContainer: {
display: "flex",
flexDirection: "column",
padding: theme.spacing.lg + 4,
paddingHorizontal: 56 / 2 - 8,
paddingTop: 0,
width: "100%",
backgroundColor: "transparent",
overflow: "hidden",
},
row: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
},
center: {
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
},
column: {
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "flex-start",
height: "100%",
width: "100%",
position: "relative",
padding: 24,
},
linkIconContainer: {
position: "absolute",
right: 16,
bottom: 16,
},
textBody: {
...theme.font.body,
color: theme.colors.white.white50,
},
textBodySecondary: {
marginLeft: 8,
...theme.font.body,
color: theme.colors.white.white75,
},
});
================================================
FILE: src/screens/Search/components/LeftSearchContent.tsx
================================================
import React from "react";
import { Text, View } from "react-native";
import * as WebBrowser from "expo-web-browser";
import ELetterSVG from "@react-native-ios/assets/svg/e-letter.svg";
import GithubSVG from "@react-native-ios/assets/svg/github.svg";
import LinkSVG from "@react-native-ios/assets/svg/link.svg";
import MessageSVG from "@react-native-ios/assets/svg/message.svg";
import WidgetItem from "@react-native-ios/components/WidgetItem";
import AnimatedInput from "@react-native-ios/components/AnimatedInput";
import styles from "./LeftSearchContent.styles";
import theme from "@react-native-ios/constants/theme";
export default function LeftSearchContent() {
const handleNavigateWebsite = async () => {
await WebBrowser.openBrowserAsync("https://ozturkenes.com");
};
const handleNavigateToGithub = async () => {
await WebBrowser.openBrowserAsync("https://github.com/enesozturk");
};
return (
<>
Did you like this app? Give me a star and follow me on Github for
more ⭐️
enesozturk/react-native-ios
You need a React & React Native developer? Contact me!
enesozturk.d@gmail.com
>
);
}
================================================
FILE: src/screens/Search/index.ts
================================================
export { default } from "./Search";
================================================
FILE: src/utils/swipeableGestureHandlers.ts
================================================
import { PanGestureHandlerEventPayload } from "react-native-gesture-handler";
import { cancelAnimation, SharedValue } from "react-native-reanimated";
import { handleOnEndHorizontal } from "@react-native-ios/configs/horizontalGestureCalculations";
import { handleOnEndVertical } from "@react-native-ios/configs/verticalGestureCalculations";
type GestureHandlerStartProps = {
direction: SharedValue;
e: PanGestureHandlerEventPayload;
startX: SharedValue;
};
type GestureHandlerProps = {
direction: SharedValue;
e: PanGestureHandlerEventPayload;
offsetY?: SharedValue;
startX: SharedValue;
offsetX: SharedValue;
};
export const handleGestureOnStart = ({
e,
direction,
startX,
}: GestureHandlerStartProps) => {
"worklet";
cancelAnimation(startX);
direction.value = Math.abs(e.translationX) > Math.abs(e.translationY) ? 1 : 0;
};
export const handleGestureOnUpdate = ({
direction,
offsetY,
offsetX,
startX,
e,
}: GestureHandlerProps) => {
"worklet";
if (direction.value) {
cancelAnimation(startX);
offsetX.value = e.translationX + startX.value;
} else {
if (offsetY) offsetY.value = Math.max(0, e.translationY);
}
};
export const handleGestureOnEnd = ({
direction,
offsetY,
offsetX,
startX,
e,
destination,
}: GestureHandlerProps & { destination: SharedValue }) => {
"worklet";
if (direction.value) {
handleOnEndHorizontal({
e,
startX,
offsetX,
destination,
});
} else {
if (offsetY)
handleOnEndVertical({
e,
offsetY,
});
}
};
================================================
FILE: tsconfig.json
================================================
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"baseUrl": "./src",
"paths": {
"@react-native-ios/api/*": ["./api/*"],
"@react-native-ios/assets/*": ["./assets/*"],
"@react-native-ios/configs/*": ["./configs/*"],
"@react-native-ios/constants/*": ["./constants/*"],
"@react-native-ios/components/*": ["./components/*"],
"@react-native-ios/helpers/*": ["./helpers/*"],
"@react-native-ios/hooks/*": ["./hooks/*"],
"@react-native-ios/jest/*": ["./jest/*"],
"@react-native-ios/navigators/*": ["./navigators/*"],
"@react-native-ios/providers/*": ["./providers/*"],
"@react-native-ios/screens/*": ["./screens/*"],
"@react-native-ios/store/*": ["./store/*"],
"@react-native-ios/storybook/*": ["./storybook/*"],
"@react-native-ios/translations/*": ["./translations/*"],
"@react-native-ios/utils/*": ["./utils/*"]
}
}
}