React Native Expoで位置情報アプリを作る Part 3
2021-05-16
Building a geolocation app with React Native Expo. Part 3
前回の続きです。リリースした新作アプリFind Good Oneの仕組みやコードを書きます。
Firebaseに接続する
Firebase接続用のAPI keyを記述したファイルを作ります。
firebase.js
import * as firebase from 'firebase';
import "firebase/auth"
import "firebase/firestore"
const firebaseConfig = {
apiKey: "見せられないよ",
authDomain: "見せられないよ",
projectId: "見せられないよ",
storageBucket: "見せられないよ",
messagingSenderId: "見せられないよ",
appId: "見せられないよ",
measurementId: "見せられないよ"
};
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
}
export { firebase };
権限取得
FGOでは
- 画像アップロードのためのフォトライブラリへのアクセス(iOSのみ)
- プッシュ通知の送信
- フォアグラウンドとバックグラウンドの位置情報の取得
の権限が必要です。プッシュ通知は必要ありませんが、フォトライブラリと位置情報の取得は記述しておく必要があります。
app.json
"ios": {
"supportsTablet": true,
"bundleIdentifier": "net.votepurchase.findgoodone",
"buildNumber": "1.0.7",
"config": {
"googleMapsApiKey": "見せられないよ。アプリでは使ってないけど一応設定してあるよ"
},
"infoPlist": {
"NSPhotoLibraryUsageDescription": "プロフィールのアバター画像と宝箱の画像を変更するためにフォトライブラリーを使用します",
"NSLocationAlwaysAndWhenInUseUsageDescription": "宝箱の設置と他のユーザーが設置した宝箱を探すために位置情報を使用します",
"NSLocationWhenInUseUsageDescription": "宝箱を設置するために位置情報を使用します",
"NSLocationAlwaysUsageDescription": "他のユーザーが設置した宝箱を探すために位置情報を使用します",
"UIBackgroundModes": [
"location",
"fetch"
]
}
},
"android": {
"package": "net.votepurchase.fgo",
"versionCode": 10,
"googleServicesFile": "./google-services.json",
"config": {
"googleMaps": {
"apiKey": "見せられないよ"
}
},
"permissions": [
"ACCESS_COARSE_LOCATION",
"ACCESS_FINE_LOCATION",
"ACCESS_BACKGROUND_LOCATION",
"FOREGROUND_SERVICE"
]
},
iOSの場合UIBackgroundModes.location
はバックグラウンドの位置情報を取得するために必要です。fetch
は移動するたびにジオフェンスを更新する機能を実装しようとしたときの名残で残してあります。
Androidの場合は4つ全てが必要です。
画面遷移
画面遷移のコードを紹介します。PINE proのときとほぼ同じです。一点、Expo SDK 41でexpo-permissions
が非推奨になったのでプッシュ通知のパーミッション取得部分のコードが少し違っています。
src\routes\navigation\Navigation.js
まずはコード全体です。
import 'react-native-gesture-handler';
import React, { useEffect, useState } from 'react'
import { firebase } from '../../../firebase'
import { colors } from 'theme'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import FontIcon from 'react-native-vector-icons/FontAwesome5'
import Login from '../../scenes/login'
import Registration from '../../scenes/registration'
import Home from '../../scenes/home'
import Treasure from '../../scenes/treasure'
import Local from '../../scenes/location'
import Profile from '../../scenes/profile'
import Map from '../../scenes/map'
import Set from '../../scenes/set'
import Items from '../../scenes/items'
import Item from '../../scenes/item'
import Scan from '../../scenes/scan'
import Discover from '../../scenes/discover'
import * as Notifications from 'expo-notifications'
// import DrawerNavigator from './drawer'
import {decode, encode} from 'base-64'
if (!global.btoa) { global.btoa = encode }
if (!global.atob) { global.atob = decode }
const Stack = createStackNavigator()
const Tab = createBottomTabNavigator()
const navigationProps = {
headerTintColor: 'white',
headerStyle: { backgroundColor: colors.darkPurple },
headerTitleStyle: { fontSize: 18 },
}
export default function App() {
const [loading, setLoading] = useState(true)
const [user, setUser] = useState(null)
useEffect(() => {
const usersRef = firebase.firestore().collection('users');
firebase.auth().onAuthStateChanged(user => {
if (user) {
usersRef
.doc(user.uid)
.onSnapshot(function(document) {
const userData = document.data()
setLoading(false)
setUser(userData)
})
} else {
setLoading(false)
}
});
}, []);
(async () => {
const { status: existingStatus } = await Notifications.getPermissionsAsync()
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") {
return;
}
const token = await Notifications.getExpoPushTokenAsync();
await firebase.firestore().collection("tokens").doc(user.email).set({ token: token.data, email: user.email })
})();
if (loading) {
return (
<></>
)
}
const HomeNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Home">
{props => <Home {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Treasure">
{props => <Treasure {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Location">
{props => <Local {...props} extraData={user} />}
</Stack.Screen>
</Stack.Navigator>
)
}
const LoginNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Login" component={Login} />
<Stack.Screen name="Registration" component={Registration} />
</Stack.Navigator>
)
}
const ProfileNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Profile">
{props => <Profile {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Map">
{props => <Map {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Set">
{props => <Set {...props} extraData={user} />}
</Stack.Screen>
</Stack.Navigator>
)
}
const ItemsNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Items">
{props => <Items {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Item">
{props => <Item {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Location">
{props => <Local {...props} extraData={user} />}
</Stack.Screen>
</Stack.Navigator>
)
}
const DiscoverNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Scan">
{props => <Scan {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Discover">
{props => <Discover {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Location">
{props => <Local {...props} extraData={user} />}
</Stack.Screen>
</Stack.Navigator>
)
}
const TabNavigator = () => (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused }) => {
switch (route.name) {
case 'Home':
return (
<FontIcon
name="home"
color={focused ? colors.lightPurple : colors.gray}
size={20}
solid
/>
)
case 'Items':
return (
<FontIcon
name="gift"
color={focused ? colors.lightPurple : colors.gray}
size={20}
solid
/>
)
case 'Discover':
return (
<FontIcon
name="map-marker-alt"
color={focused ? colors.lightPurple : colors.gray}
size={20}
solid
/>
)
case 'Profile':
return (
<FontIcon
name="user"
color={focused ? colors.lightPurple : colors.gray}
size={20}
solid
/>
)
default:
return <View />
}
},
})}
tabBarOptions={{
activeTintColor: colors.lightPurple,
inactiveTintColor: colors.gray,
}}
initialRouteName="Home"
swipeEnabled={false}
>
<Tab.Screen name="Home" component={HomeNavigator} />
<Tab.Screen name="Items" component={ItemsNavigator} />
<Tab.Screen name="Discover" component={DiscoverNavigator} />
<Tab.Screen name="Profile" component={ProfileNavigator} />
</Tab.Navigator>
)
return(
<NavigationContainer>
{ user ? (
<TabNavigator/>
) : (
<LoginNavigator/>
)}
</NavigationContainer>
)
}
ログイン状態の判定とユーザー情報の取得
const [user, setUser] = useState(null) //ユーザー情報格納用のフックを定義
コンポーネントマウント時にFirebaseのauth
メソッドでログイン状態を判定します。ログイン状態であればユーザー情報を取得します。onSnapshot
で取得することによりユーザー情報の更新をリアルタイムでリッスンします。これによって宝箱の取得や削除、プロフィールの編集時に画面に反映させます。
useEffect(() => {
const usersRef = firebase.firestore().collection('users');
firebase.auth().onAuthStateChanged(user => {
if (user) {
usersRef
.doc(user.uid)
.onSnapshot(function(document) {
const userData = document.data()
setLoading(false)
setUser(userData)
})
} else {
setLoading(false)
}
});
}, []);
プッシュ通知のパーミッションとトークンを取得
(async () => {
const { status: existingStatus } = await Notifications.getPermissionsAsync()
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") {
return;
}
const token = await Notifications.getExpoPushTokenAsync();
await firebase.firestore().collection("tokens").doc(user.email).set({ token: token.data, email: user.email })
})();
各スタックを定義
前回解説した5つのスタックを定義していきます。
const HomeNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Home">
{props => <Home {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Treasure">
{props => <Treasure {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Location">
{props => <Local {...props} extraData={user} />}
</Stack.Screen>
</Stack.Navigator>
)
}
const LoginNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Login" component={Login} />
<Stack.Screen name="Registration" component={Registration} />
</Stack.Navigator>
)
}
const ProfileNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Profile">
{props => <Profile {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Map">
{props => <Map {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Set">
{props => <Set {...props} extraData={user} />}
</Stack.Screen>
</Stack.Navigator>
)
}
const ItemsNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Items">
{props => <Items {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Item">
{props => <Item {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Location">
{props => <Local {...props} extraData={user} />}
</Stack.Screen>
</Stack.Navigator>
)
}
const DiscoverNavigator = () => {
return (
<Stack.Navigator headerMode="screen" screenOptions={navigationProps}>
<Stack.Screen name="Scan">
{props => <Scan {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Discover">
{props => <Discover {...props} extraData={user} />}
</Stack.Screen>
<Stack.Screen name="Location">
{props => <Local {...props} extraData={user} />}
</Stack.Screen>
</Stack.Navigator>
)
}
ログイン後のスタックをTabNavigatorとしてラップ
const TabNavigator = () => (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused }) => {
switch (route.name) {
case 'Home':
return (
<FontIcon
name="home"
color={focused ? colors.lightPurple : colors.gray}
size={20}
solid
/>
)
case 'Items':
return (
<FontIcon
name="gift"
color={focused ? colors.lightPurple : colors.gray}
size={20}
solid
/>
)
case 'Discover':
return (
<FontIcon
name="map-marker-alt"
color={focused ? colors.lightPurple : colors.gray}
size={20}
solid
/>
)
case 'Profile':
return (
<FontIcon
name="user"
color={focused ? colors.lightPurple : colors.gray}
size={20}
solid
/>
)
default:
return <View />
}
},
})}
tabBarOptions={{
activeTintColor: colors.lightPurple,
inactiveTintColor: colors.gray,
}}
initialRouteName="Home"
swipeEnabled={false}
>
<Tab.Screen name="Home" component={HomeNavigator} />
<Tab.Screen name="Items" component={ItemsNavigator} />
<Tab.Screen name="Discover" component={DiscoverNavigator} />
<Tab.Screen name="Profile" component={ProfileNavigator} />
</Tab.Navigator>
)
判定したログイン状態によって表示する画面を変える
ユーザー情報が格納されてるかを三項演算子で評価してLoginNavigator
かTabNavigator
を表示します。
return(
<NavigationContainer>
{ user ? (
<TabNavigator/>
) : (
<LoginNavigator/>
)}
</NavigationContainer>
)
まとめ
PINE proのときとほぼ同じでした。