React Navigation 6で全画面モーダルを使う
2022-02-02
Full screen modal on React Navigation 6
React Navagation
React Nativeの定番ナビゲーションライブラリReact Navigation(version 6)でiOS風の全画面モーダルを実装する方法です。
特に、ボトムタブナビゲーションと全画面モーダルを組み合わせているときに、モーダル画面でボトムタブを表示したくない場合は複数のネストが必要になるためブログ記事として残します。
レポジトリはこちら → ReactNativeExpoBoilerplate
作成する各画面
- ホーム画面です。ここから各画面に遷移します。
- 全画面モーダル画面です。1の画面から遷移します。ボトムタブは表示しません。「From: Home screen」と表示しています。どの画面から遷移してきたのかを画面遷移時にパラーメーターで渡しています。
- 全画面モーダル画面その2です。2の画面から遷移します。全画面モーダル内のスタックです。ここでも、どの画面から遷移してきたのかを画面遷移時にパラーメーターで渡しています。
- 全画面モーダルその3です。1の画面から遷移します。全画面モーダルですがボトムタブを表示しています。
ナビゲーションの構成
- Navigation(大元の親コンポーネント)
- RootStack(ここでボトムタブとボトムタブなしのモーダル画面を分岐)
- HomeRoot(ボトムタブ)
- HomeStacks
- Home
- Details
- Menu(ボトムタブありのモーダル)
- ProfileStacks
- ReadWriteTab
- HomeStacks
- ModalStack (ボトムタブなしのモーダルスタック)
- Modal
- HomeRoot(ボトムタブ)
- RootStack(ここでボトムタブとボトムタブなしのモーダル画面を分岐)
各画面のコード
Navigation
まずは大元の親コンポーネントを作ります。RootStack
をNavigationContainer
でラップするためのコンポーネントです。
import React from 'react'
import { NavigationContainer } from '@react-navigation/native'
import RootStack from './RootStack'
export default () => {
return (
<NavigationContainer>
<RootStack />
</NavigationContainer>
)
}
RootStack
次にRootStack
を作ります。ここでボトムタブナビゲーターと全画面モーダル用のスタックを分岐させます。
import React, { useState, useContext } from "react";
import { createStackNavigator } from '@react-navigation/stack'
import TabNavigator from "./tabs/Tabs";
import { ModalStacks } from "./stacks/ModalStacks";
const Stack = createStackNavigator()
export default function RootStack() {
return (
<Stack.Navigator
screenOptions={{
headerShown: false
}}
>
<Stack.Screen
name='HomeRoot'
component={TabNavigator}
/>
<Stack.Group
screenOptions={{
presentation: 'modal',
headerShown: false,
}}
>
<Stack.Screen
name='ModalStack'
component={ModalStacks}
/>
</Stack.Group>
</Stack.Navigator>
)
}
ModalStacks
次に全画面モーダル用のスタックを作ります。
import React, { useState, useContext } from "react";
import { createStackNavigator } from '@react-navigation/stack'
import Modal from '../../../scenes/modal/Modal'
import Print from '../../../scenes/print/Print'
const Stack = createStackNavigator()
export const ModalStacks = () => {
return (
<Stack.Navigator
screenOptions={{
headerShown: true,
}}
>
<Stack.Screen
name='Modal'
component={Modal}
/>
<Stack.Screen
name='Print'
component={Print}
/>
</Stack.Navigator>
)
}
Tabs
ボトムタブコンポーネントを作成します。通常通りcreateBottomTabNavigator()
で各スタックをラップしていきます。
import React from 'react'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import FontIcon from 'react-native-vector-icons/FontAwesome5'
import { colors } from 'theme'
// stack navigators
import { HomeStacks } from '../stacks/HomeStacks'
import { ProfileStacks } from '../stacks/ProfileStacks'
import { ReadWriteStacks } from '../stacks/ReadWriteStacks'
const Tab = createBottomTabNavigator()
const TabNavigator = () => {
return (
<Tab.Navigator
options={{
tabBarActiveTintColor: colors.lightPurple,
tabBarInactiveTintColor: colors.gray,
}}
defaultScreenOptions={{
headerShown:false,
headerTransparent:true
}}
screenOptions={({ route }) => ({
headerShown: false,
})}
initialRouteName="HomeTab"
swipeEnabled={false}
>
<Tab.Screen
name="HomeTab"
component={HomeStacks}
options={{
tabBarLabel: 'Home',
tabBarIcon: ({ color, size }) => (
<FontIcon
name="home"
color={color}
size={size}
/>
),
}}
/>
<Tab.Screen
name="ProfileTab"
component={ProfileStacks}
options={{
tabBarLabel: 'Profile',
tabBarIcon: ({ color, size }) => (
<FontIcon
name="user"
color={color}
size={size}
/>
),
}}
/>
<Tab.Screen
name="ReadWriteTab"
component={ReadWriteStacks}
options={{
tabBarLabel: 'R/W',
tabBarIcon: ({ color, size }) => (
<FontIcon
name="address-card"
color={color}
size={size}
/>
),
}}
/>
</Tab.Navigator>
)
}
export default TabNavigator
HomeStacks
次にボトムタブありのモーダル画面を含むスタックを作ります。
import React from 'react'
import { createStackNavigator } from '@react-navigation/stack'
import { navigationProps } from './navigationProps/navigationProps'
import Home from '../../../scenes/home'
import Details from '../../../scenes/details'
import Menu from '../../../scenes/menu/Menu'
const Stack = createStackNavigator()
export const HomeStacks = () => {
return (
<Stack.Navigator
initialRouteName="Home"
screenOptions={navigationProps}
>
<Stack.Screen
name="Home"
component={Home}
options={({ navigation }) => ({
title: 'Home',
headerShown: false
})}
/>
<Stack.Screen
name="Details"
component={Details}
options={({ navigation }) => ({
title: 'Details',
})}
/>
<Stack.Group
screenOptions={{
presentation: 'modal',
headerShown: false
}}
>
<Stack.Screen
name="Menu"
component={Menu}
options={({ navigation }) => ({
title: 'Menu',
})}
/>
</Stack.Group>
</Stack.Navigator>
)
}
画面間での値の受け渡し方法
通常、スクリーン間での値の受け渡しはuseNavigation
とuseRoute
を使って以下のように受け渡しをすることが多いと思います。
import { useNavigation } from '@react-navigation/native'
const navigation = useNavigation()
onPress={() => {
navigation.navigate('Details', { from: 'Home' })
}}
しかし、ホーム画面(HomeRoot内)から全画面モーダル画面(ModalStack)のように、別スタックに遷移する場合は以下のようにします。遷移先のスタックとスクリーン名をそれぞれ指定してあげるのと、パラメーターの書き方が通常と少し異なります。
onPress={() => {
navigation.navigate('ModalStack', {
screen: 'Modal',
params: {from: 'Home'}
})
}}
受け取り側は通常通りです。
import { useRoute } from '@react-navigation/native'
const route = useRoute()
const { from } = route.params
Androidの追加設定
ここまでのコードだとAndroidにおいて全画面モーダルの表現がiOS風になっていません。
追加の設定を行ってAndroidでもiOSと同じようにモーダルを表現するようにします。
RootStack
import { TransitionPresets } from "@react-navigation/stack";
<Stack.Group
screenOptions={{
presentation: 'modal',
headerShown: false,
/* ここから */
gestureEnabled: true,
cardOverlayEnabled: true,
...TransitionPresets.ModalPresentationIOS,
/* ここまで追加 */
}}
>
まとめ
これでReact NativeでもiOS風の全画面モーダルを実装できました。