728x90
반응형

animated value를 사용시에 항상

 

const animatedY = useRef(new Animated.Value(0)).current;

이런식으로 useRef를 사용한다.

이는 데이터영역의 state 값들이 변화시에 컴포넌트의 리랜더링이 필수적으로 일어날수밖에 없는 상황에 

애니메이션값을 초기화시키지 않기 위함이다.

 

현재 구현한 애니메이션은 스크롤에 따라 opacity 가 변하고, 헤더의 위치및 높이가 변하고 등등

 

animated.timing 함수를 사용하는 부분과, Animated event 를 사용하는 부분으로 나누어져있다.

 

가끔 0번째에서 첫번째로 넘어갈때, 애니메이션이 끊기는 현상이 발생하여 디버깅해보니,

리랜더링시에 애니메이션이 초기화되서 그런가 싶었던 이유와는 별개로, nativeDriver를 사용하는 쓰레드에서 에러가 생기는것 같았다.

 

따라서 useNativeDriver 을 false 로 주니, 해결되었다.

 

useNativeDriver는 애니메이션 처리 작업을 자바스크립트 엔진이 아닌 네이티브 레벨에서 진행하게 하는 옵션으로 transform, opacity처럼 레이아웃과 관련없는 스타일에만 적용할 수 있다. 예를 들어 레이아웃에 영향을 끼치는 left, width, paddingLeft, marginLeft와 같은 스타일에는 꼭 useNativeDriver를 false로 지정해야 한다.

 

 

728x90
반응형
728x90
반응형

style 에 borderRadius 를 적용하면, 안드로이드에서는 radius가 적용되지만, ios에는 적용이 제대로 되지않는다.

 

<ImageBackground
     style={{
     width: deviceWidth - 2 * wrapperPadding,
     height: cardHeight,
     }}
     imageStyle={{ borderRadius: 15 }}
     source={require('../../assets/example/banner.png')}
></ImageBackground>

 

imageStyle props 에 넣어주면 된다.

728x90
반응형
728x90
반응형

피그마에 적혀있는 fontsize 대로 text style 에 입력하게되면, 기본 os의 글자크기에 영향을 많이 받는다.

 

const { fontScale } = Dimensions.get('window');
export const resizeFont = (fontSize: number) => {
return fontSize / fontScale;
};

로 함수를 만들고

 

해당하는 함수에 figma에 적혀있는 사이즈를 넣으면 os의 크기와 별개로 크기정렬이 잘된다.

728x90
반응형
728x90
반응형

Flatlist 나 scrollview에서 pagingEnabled 를 하면, snap 하는 느낌은 들지만, scroll의 offset 이 맞지않는 경우가 많다..

 

이를 위해

 

const offset = heightScale(600) + 2 * cardVerticalMargin;
const snapToOffsets = useMemo(
() =>
Array.from(Array(DUMMY.length)).map(
(_, index) => index * offset - headerHeight,
),
[DUMMY],
);
 

이런식으로 사용하면,

 

어느 offset들에게 걸릴지가 snapToOffesets props 에 의해 flatlist / scrollview 에 전달된다.

 

pagingEnabled
decelerationRate="fast"
scrollEventThrottle={16}
snapToOffsets={snapToOffsets}

주로 같이쓰는 옵션들이다.

728x90
반응형
728x90
반응형

 

이번 앱 디자인에는 animated 를 많이 사용한 UI 가 들어간다.

 

animated view 의 핵심은, state로 ui를 변화시키는 것을 지양하고, 

 

const animatedY = useRef(new Animated.Value(0)).current;

처럼 ref로 관리해줘야 리랜더링을 방지하여 매끄러운 애니메이션 효과가 나온다.

 

기본 사용은 

 

const translateY = animatedY.interpolate({
inputRange: [0, bannerHeight - headerHeight - safeAreaTop],
outputRange: [0, -bannerHeight + headerHeight + safeAreaTop],
extrapolate: 'clamp',
});

이런 형태로, translate를 한번해주어, 스크롤 input과 output 의 레인지를 계산해주고(해당 코드는 헤더가 스크롤할때 아래로 이동하도록 하는 코드이다)

 

clamp로 묶어주어, input range 의 밖의 값이 입력되었을때, output range의 큰 값이 출력되도록 해준다.

 

애니메이션 효과를 입히고 싶은 컴포넌트에 

 

<Animated.View
style={{
flexDirection: 'column',
transform: [{ translateY: animatedY }],
}}
>

해당 형태로 추가해준다.

 

animated.timing 으로 시작한 애니메이션도 있고, scroll을 통한 offset으로 해당하는 애니메이션도 있어 풀 소스코드를 올려놓는다.

 

const safeAreaTop = useSafeAreaInsets().top;

를 통해 ios safeArea에도 대처했다.

 

import { useCallback, useRef } from 'react';
import {
Animated,
Dimensions,
NativeScrollEvent,
NativeSyntheticEvent,
Platform,
Text,
UIManager,
View
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { heightScale } from '../../util/Layout';
import { HomeHeader } from './HomeHeader';

const DUMMY = [
{
index: 0,
},
{
index: 1,
},
{
index: 2,
},
{
index: 3,
},
{
index: 4,
},
{
index: 5,
},
{
index: 6,
},
{
index: 7,
},
];

const deviceWidth = Dimensions.get('window').width;
const bannerHeight = (deviceWidth * 600) / 480;
const headerHeight = heightScale(75);

if (
Platform.OS === 'android' &&
UIManager.setLayoutAnimationEnabledExperimental
) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}

export const HomeScreen = () => {
let currentOffset = 0;
const animatedY = useRef(new Animated.Value(0)).current;
const animatedBackground = useRef(new Animated.Value(0)).current;
const animatedOpacity = useRef(new Animated.Value(0)).current;

const safeAreaTop = useSafeAreaInsets().top;

const animationStart = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
let direction =
event.nativeEvent.contentOffset.y > currentOffset ? 'down' : 'up';
currentOffset = event.nativeEvent.contentOffset.y;

if (
direction === 'down' &&
event.nativeEvent.contentOffset.y > bannerHeight / 3
) {
console.log('down');
Animated.parallel([
Animated.timing(animatedBackground, {
useNativeDriver: true,
duration: 300,
toValue: 1,
}),
Animated.timing(animatedOpacity, {
useNativeDriver: true,
duration: 300,
toValue: 1,
}),
]).start();
}
if (
direction === 'up' &&
event.nativeEvent.contentOffset.y < bannerHeight / 3
) {
console.log('up');
Animated.parallel([
Animated.timing(animatedBackground, {
useNativeDriver: true,
duration: 300,
toValue: 0,
}),
Animated.timing(animatedOpacity, {
useNativeDriver: true,
duration: 300,
toValue: 0,
}),
]).start();
}
},
[],
);

// const transY = Animated.
const translateY = animatedY.interpolate({
inputRange: [0, bannerHeight - headerHeight - safeAreaTop],
outputRange: [0, -bannerHeight + headerHeight + safeAreaTop],
extrapolate: 'clamp',
});

const translateHeaderY = animatedY.interpolate({
inputRange: [0, bannerHeight - headerHeight - safeAreaTop],
outputRange: [0, bannerHeight - headerHeight - safeAreaTop],
extrapolate: 'clamp',
});

const onScroll = Animated.event(
[{ nativeEvent: { contentOffset: { y: animatedY } } }],
{
useNativeDriver: true,
listener: (event) => {
animationStart(event);
},
},
);

return (
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
}}
>
<Animated.FlatList
style={{ flex: 1 }}
data={DUMMY}
ListHeaderComponent={() => {
return (
<HomeHeader
animatedY={translateY}
translateHeaderY={translateHeaderY}
safeAreaTop={safeAreaTop}
animatedBackground={animatedBackground}
bannerHeight={bannerHeight}
animatedOpacity={animatedOpacity}
headerHeight={headerHeight}
/>
);
}}
onScroll={onScroll}
// pagingEnabled
stickyHeaderIndices={[0]}
renderItem={({ item, index }) => {
return (
<View
style={{
width: 100,
height: heightScale(600),
borderWidth: 1,
backgroundColor: 'pink',
}}
>
<Text style={{ fontSize: 40 }}>{index}</Text>
</View>
);
}}
/>
</View>
);
};
import React from 'react';
import {
Animated,
Dimensions,
Image,
TouchableOpacity,
View,
} from 'react-native';
import AlarmBlack from '../../assets/icons/alarmBlack.svg';
import AlarmWhite from '../../assets/icons/alarmWhite.svg';
import { heightScale, widthScale } from '../../util/Layout';

type HomeHeaderProps = {
animatedY: Animated.AnimatedInterpolation<string | number>;
translateHeaderY: Animated.AnimatedInterpolation<string | number>;
safeAreaTop: number;
animatedBackground: Animated.Value;
bannerHeight: number;
animatedOpacity: Animated.Value;
headerHeight: number;
};
const deviceWidth = Dimensions.get('window').width;

export const HomeHeader = ({
animatedY,
translateHeaderY,
safeAreaTop,
animatedBackground,
bannerHeight,
animatedOpacity,
headerHeight,
}: HomeHeaderProps) => {
return (
<Animated.View
style={{
flexDirection: 'column',
transform: [{ translateY: animatedY }],
}}
>
<Animated.View
style={{
position: 'absolute',
top: safeAreaTop,
width: deviceWidth,
height: headerHeight,
zIndex: 1,
transform: [{ translateY: translateHeaderY }],
backgroundColor: animatedBackground.interpolate({
inputRange: [0, 1],
outputRange: ['#FFFFFF00', '#FFFFFF'],
}),
alignItems: 'center',
justifyContent: 'center',
}}
>
<Image
source={require('../../assets/images/headerLogo.png')}
style={{
width: widthScale(50),
height: heightScale(35),
}}
resizeMode="contain"
/>
<TouchableOpacity
style={{
width: headerHeight,
height: headerHeight,
position: 'absolute',
right: 0,
justifyContent: 'center',
alignItems: 'center',
top: 0,
}}
>
<Animated.View
style={{
zIndex: 2,
position: 'absolute',
opacity: animatedOpacity,
}}
>
<AlarmBlack width={widthScale(40)} height={widthScale(40)} />
</Animated.View>
<AlarmWhite width={widthScale(40)} height={widthScale(40)} />
</TouchableOpacity>
</Animated.View>

<TouchableOpacity
style={{
width: deviceWidth,
height: bannerHeight,
backgroundColor: 'orange',
}}
></TouchableOpacity>
<View
style={{
width: '100%',
height: headerHeight,
backgroundColor: 'blue',
}}
></View>
<View
style={{
width: '100%',
height: headerHeight,
backgroundColor: 'red',
}}
></View>
</Animated.View>
);
};
728x90
반응형
728x90
반응형
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: positionY } } }],
{
useNativeDriver: true,
listener: (event) => {
animationStart(event);
},
},
)}

기존 리스너함수를 listener 로 명시하여 사용하면 된다.

728x90
반응형
728x90
반응형

ios에서 노치디자인에서 헤더를 투명화 하기위해

 

헤더의 높이를 조절해야 할 일이 생겼다.

 

animated view로 헤더의 position을

transform: [
{
translateY: safeAreaTopPosition.interpolate({
inputRange: [0, 1],
outputRange: [0, insets.top],
}),
},
],

로 옮겨주는데,

 

그렇게되면 status bar 에 겹치는 현상이 발생하기때문에, height 값조정과 내용들의 정렬화 애니메이션이 필요하게되었다.

 

height 값은 state값으로 조절해주고

 

setState에서 값을 변경 한뒤

 

LayoutAnimation.configureNext(LayoutAnimation.Presets.linear);

를 통하면 자연스럽게 늘어나는 UI를 입혀준다.

 

최종 소스는

 

import { useCallback, useRef, useState } from 'react';
import {
Animated,
Dimensions,
FlatList,
LayoutAnimation,
NativeScrollEvent,
NativeSyntheticEvent,
Platform,
View,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { heightScale } from '../../util/Layout';

const DUMMY = [
{
index: 0,
},
{
index: 1,
},
{
index: 2,
},
{
index: 3,
},
{
index: 4,
},
{
index: 5,
},
{
index: 6,
},
{
index: 7,
},
];

const deviceWidth = Dimensions.get('window').width;
const bannerHeight = (deviceWidth * 600) / 480;

const ListHeader = () => {
return (
<View style={{ flexDirection: 'column' }}>
<View
style={{
width: deviceWidth,
height: bannerHeight,
backgroundColor: 'orange',
}}
></View>
<View
style={{
width: '100%',
height: heightScale(90),
backgroundColor: 'blue',
}}
></View>
<View
style={{
width: '100%',
height: heightScale(90),
backgroundColor: 'red',
}}
></View>
</View>
);
};

export const HomeScreen = () => {
const viewRef = useRef(null);
var currentOffset = 0;
const backgroundColor = useRef(new Animated.Value(0)).current;
const safeAreaTopPosition = useRef(new Animated.Value(1)).current;
const safeAreaTopPadding = useRef(new Animated.Value(0)).current;
const [animatedHeaderHeight, setAnimatedHeaderHeight] = useState(
heightScale(75),
);
const insets = useSafeAreaInsets();
const fadingIn = useCallback(() => {
Animated.timing(backgroundColor, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
if (Platform.OS === 'ios') {
Animated.timing(safeAreaTopPosition, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start();
Animated.timing(safeAreaTopPadding, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
setAnimatedHeaderHeight(heightScale(75) + insets.top);
LayoutAnimation.configureNext(LayoutAnimation.Presets.linear);
}
}, []);
const fadingOut = useCallback(() => {
Animated.timing(backgroundColor, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start();
if (Platform.OS === 'ios') {
Animated.timing(safeAreaTopPosition, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
Animated.timing(safeAreaTopPadding, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start();
setAnimatedHeaderHeight(heightScale(75));
LayoutAnimation.configureNext(LayoutAnimation.Presets.linear);
}
}, []);

const animationStart = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
let direction =
event.nativeEvent.contentOffset.y > currentOffset ? 'down' : 'up';
currentOffset = event.nativeEvent.contentOffset.y;

if (
direction === 'down' &&
event.nativeEvent.contentOffset.y > bannerHeight / 2
) {
fadingIn();
}
if (
direction === 'up' &&
event.nativeEvent.contentOffset.y < bannerHeight / 2
) {
fadingOut();
}
},
[],
);

return (
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
}}
>
<Animated.View
ref={viewRef}
style={{
width: '100%',
borderWidth: 1,
height: animatedHeaderHeight,
position: 'absolute',
top: 0,
zIndex: 1,
backgroundColor: backgroundColor.interpolate({
inputRange: [0, 1],
outputRange: ['#FFFFFF00', '#FFFFFF'],
}),
transform: [
{
translateY: safeAreaTopPosition.interpolate({
inputRange: [0, 1],
outputRange: [0, insets.top],
}),
},
],
}}
>
<Animated.Text
style={[
{ color: 'black' },
{
transform: [
{
translateY: safeAreaTopPosition.interpolate({
inputRange: [0, 1],
outputRange: [insets.top, 0],
}),
},
],
},
]}
>
headerheaderheaderheaderheader
</Animated.Text>
</Animated.View>
<FlatList
style={{ flex: 1 }}
data={DUMMY}
ListHeaderComponent={ListHeader}
onScroll={(event) => animationStart(event)}
renderItem={() => {
return (
<View
style={{
width: 100,
height: 100,
borderWidth: 1,
backgroundColor: 'pink',
}}
></View>
);
}}
/>
</View>
);
};
728x90
반응형
728x90
반응형

이번에 애니메이션이 많이 들어간 앱을 개발하면서, 작업에 재미가 붙고있다.

 

 

 

해당 코드는

 

import { useCallback, useRef } from 'react';
import {
Animated,
Dimensions,
FlatList,
NativeScrollEvent,
NativeSyntheticEvent,
Text,
View,
} from 'react-native';
import { heightScale } from '../../util/Layout';

const DUMMY = [
{
index: 0,
},
{
index: 1,
},
{
index: 2,
},
{
index: 3,
},
{
index: 4,
},
{
index: 5,
},
{
index: 6,
},
{
index: 7,
},
];

const deviceWidth = Dimensions.get('window').width;

const ListHeader = () => {
return (
<View>
<View
style={{
width: deviceWidth,
height: heightScale(630),
backgroundColor: 'orange',
}}
></View>
</View>
);
};

export const HomeScreen = () => {
const viewRef = useRef(null);
var currentOffset = 0;
const backgroundColor = useRef(new Animated.Value(0)).current;
const fadingIn = useCallback(() => {
Animated.timing(backgroundColor, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
}, []);
const fadingOut = useCallback(() => {
Animated.timing(backgroundColor, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start();
}, []);

const animationStart = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
let direction =
event.nativeEvent.contentOffset.y > currentOffset ? 'down' : 'up';
currentOffset = event.nativeEvent.contentOffset.y;

if (
direction === 'down' &&
event.nativeEvent.contentOffset.y > heightScale(630) / 2
) {
fadingIn();
}
if (
direction === 'up' &&
event.nativeEvent.contentOffset.y < heightScale(630) / 2
) {
fadingOut();
}
},
[],
);

return (
<View
style={{
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
}}
>
<Animated.View
ref={viewRef}
style={{
width: '100%',
height: heightScale(75),
borderWidth: 1,
position: 'absolute',
top: 0,
zIndex: 1,
backgroundColor: backgroundColor.interpolate({
inputRange: [0, 1],
outputRange: ['#FFFFFF00', '#FFFFFF'],
}),
}}
>
<Text style={{ color: 'black' }}>headerheaderheaderheaderheader</Text>
</Animated.View>
<FlatList
style={{ flex: 1 }}
data={DUMMY}
ListHeaderComponent={ListHeader}
onScroll={(event) => animationStart(event)}
renderItem={() => {
return (
<View
style={{
width: 100,
height: 100,
borderWidth: 1,
backgroundColor: 'pink',
}}
></View>
);
}}
/>
</View>
);
};
728x90
반응형
728x90
반응형

 

 

 

앱을 만드는 중에, 바텀탭버튼에 애니메이션 효과를 부탁받았다.

늘 Lottie파일로 구현했는데, 이번에는 

react-native-svg 로 아이콘을 보이고, 감싸는 view에 애니메이션 효과를 주면서 해결했다. 코드는

 

// import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import {
BottomTabBarButtonProps,
createBottomTabNavigator,
} from '@react-navigation/bottom-tabs';
import { StackScreenProps } from '@react-navigation/stack';
import React, { useEffect, useRef } from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import SplashScreen from 'react-native-splash-screen';
import CommunityOff from '../../assets/icons/bottomtab/communityOff.svg';
import CommunityOn from '../../assets/icons/bottomtab/communityOn.svg';
import HomeOff from '../../assets/icons/bottomtab/homeOff.svg';
import HomeOn from '../../assets/icons/bottomtab/homeOn.svg';
import MyOff from '../../assets/icons/bottomtab/myOff.svg';
import MyOn from '../../assets/icons/bottomtab/myOn.svg';
import SupportOff from '../../assets/icons/bottomtab/supportOff.svg';
import SupportOn from '../../assets/icons/bottomtab/supportOn.svg';
import TvOff from '../../assets/icons/bottomtab/tvOff.svg';
import TvOn from '../../assets/icons/bottomtab/tvOn.svg';
import { WowplanetHeader } from '../../components/WowplanetHeader';
import { RootStackParamList } from '../../navigation/RootNavigator';
import { CommunityScreen } from '../CommunityScreen';
import { HomeScreen } from '../HomeScreen';
import { MyScreen } from '../MyScreen';
import { SupportScreen } from '../SupportScreen';
import { TvScreen } from '../TvScreen';
import * as Animatable from 'react-native-animatable';
import { heightScale } from '../../util/Layout';

export type BottomTabParamList = {
HomeScreen: undefined;
SupportScreen: { userId: string };
CommunityScreen: { sort: 'latest' | 'top' } | undefined;
TvScreen: { sort: 'latest' | 'top' } | undefined;
MyScreen: undefined;
};

const TabBarIcon = ({ name, focused }: any) => {
console.log('route ::', name);
switch (name) {
case 'SupportScreen':
return (
<View
style={[
{
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
margin: 0,
},
]}
>
{focused ? (
<SupportOn width={35} height={35} />
) : (
<SupportOff width={35} height={35} />
)}
</View>
);
case 'CommunityScreen':
return (
<View
style={{
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
}}
>
{focused ? (
<CommunityOn width={35} height={35} />
) : (
<CommunityOff width={35} height={35} />
)}
</View>
);
case 'HomeScreen':
return (
<View
style={{
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
}}
>
{focused ? (
<HomeOn width={35} height={35} />
) : (
<HomeOff width={35} height={35} />
)}
</View>
);
case 'TvScreen':
return (
<View
style={{
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
}}
>
{focused ? (
<TvOn width={35} height={35} />
) : (
<TvOff width={35} height={35} />
)}
</View>
);
case 'MyScreen':
return (
<View
style={{
width: '100%',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
}}
>
{focused ? (
<MyOn width={35} height={35} />
) : (
<MyOff width={35} height={35} />
)}
</View>
);
}
};

const TabArr = [
{
route: 'SupportScreen',
label: 'SupportScreen',
component: SupportScreen,
},
{
route: 'CommunityScreen',
label: 'CommunityScreen',
component: CommunityScreen,
},
{
route: 'HomeScreen',
label: 'HomeScreen',
component: HomeScreen,
},
{
route: 'TvScreen',
label: 'TvScreen',
component: TvScreen,
},
{
route: 'MyScreen',
label: 'MyScreen',
component: MyScreen,
},
];

const TabButton = (props: BottomTabBarButtonProps) => {
const { item, onPress, accessibilityState } = props;
const focused = accessibilityState.selected;
const viewRef = useRef(null);
console.log('item :::', item);

useEffect(() => {
if (focused) {
viewRef.current.animate({
0: { scale: 0.5, rotate: '360deg' },
1: { scale: 1.0, rotate: '0deg' },
});
} else {
viewRef.current.animate({
0: { scale: 1.0, rotate: '-360deg' },
1: { scale: 1, rotate: '0deg' },
});
}
}, [focused]);

return (
<TouchableOpacity
onPress={onPress}
activeOpacity={1}
style={styles.container}
>
<Animatable.View ref={viewRef} duration={1000} style={styles.container}>
<TabBarIcon focused={focused} name={item.route} />
</Animatable.View>
</TouchableOpacity>
);
};

export const BottomTabNavigator = (
props: StackScreenProps<RootStackParamList, 'BottomTab'>,
) => {
const Tab = createBottomTabNavigator<BottomTabParamList>();
console.log('rendering BottomTabNavigator :::::::::::');

useEffect(() => {
SplashScreen.hide();
}, []);

return (
<View style={{ flex: 1 }}>
<WowplanetHeader navigation={props.navigation} route={props.route} />
<Tab.Navigator
screenOptions={() => ({
headerShown: false,
tabBarShowLabel: false,
lazy: true,
tabBarStyle: {
height: heightScale(75),
},
})}
screenListeners={({ route }) => ({
tabPress: () => {
console.log(route.name);
},
})}
initialRouteName="HomeScreen"
>
{TabArr.map((item, index) => {
return (
<Tab.Screen
key={index}
name={item.route}
component={item.component}
options={{
tabBarShowLabel: false,
tabBarButton: (props) => <TabButton {...props} item={item} />,
}}
/>
);
})}
</Tab.Navigator>
</View>
);
};
728x90
반응형
728x90
반응형

firebase ios 를 세팅하다가 해당 이슈가 계속 뜨기 시작했다.

The operation couldn’t be completed. No APNS token specified before fetching FCM Token 

 

import notifee from '@notifee/react-native';
 
const res = await notifee.requestPermission();

로 해결했다.

 

ios같은 경우 authorizationStatus 가 1이어야 정상 작동했다.

 

해당 코드에서 res.ios.authorizationStatus === 1 이 아닌경우, requestPermission을 작동시켰다.

728x90
반응형

+ Recent posts