【ReactNative】React Navigationを使ってモーダルを実装する

# まえがき

React Nativeではモーダル機能は多様なライブラリで提供されている。
本記事ではreact-navigationのモーダルに注目して調査した結果についてまとめる。

# React Navigationとは

詳しくは[Hello React Navigation | React Navigation](https://reactnavigation.org/docs/hello-react-navigation/)から。
リンク先の一部文章を直訳したのが以下文。

> React ナビゲーションのネイティブスタックナビゲーターは、アプリが画面間を切り替えてナビゲーション履歴を管理する方法を提供します。

画面遷移と、その履歴を管理する方法を提供していると理解できる。また、画面遷移時のアニメーションや、ロジックを実装できたりと多様な機能を持っている。

# 環境
```json
"react": "18.0.0",
"react-native": "0.69.5",
"@react-navigation/native": "~6.0.6",
"@react-navigation/stack": "~6.3.0",
```

# StackNavigatorを使ってモーダルを実装する
## StackNavigatorとは
[Stack Navigator | React Navigation](https://reactnavigation.org/docs/stack-navigator/)

## モーダルを実装する
StackNavigatorのScreenにoptionsというプロパティがある。画面をモーダルとして実装するには下記コード例のようにoptionsの中で`presentation: 'modal'`を指定する。

```jsx
const nav = createStackNavigator();
export const ModalStackNav: React.FC = () => {
  return (
    <nav.Navigator>
      <nav.Screen 
        name="ExampleModal"
        component={ExampleModalScreen} 
        options={{
          presentation: 'modal'
        }}
      />
    </nav.Navigator> 
  );
};
```

後述の「スタイルを整える」でモーダルのサイズを調整できるが、背景に遷移前の画面は見えない。

## 背景に遷移前の画面を表示させるために

`presentation: ‘transparentModal’`を使用する。
transparentModalはmodalと似ているが、いくつか違う点がある。
[ドキュメント](https://reactnavigation.org/docs/stack-navigator/#presentation)に記載されている点のうち以下分を引用する。

> Sets background color of the screen to transparent, so previous screen is visible
> 

直訳によると「画面の背景色を透明に設定して、前の画面が表示されるようにします」とのこと。
これにより、背景に遷移前の画面を透明で表示できる。
しかしモーダルの大きさを調整していないので、これだけでは遷移前の画面が見えない。

## スタイルを整える

モーダルのスタイルを指定し、大きさや四隅の輪郭の丸み等を調整するため、`cardStyle`にてCSSを指定する。また、`cardOverlayEnabled: true`を指定するとモーダルの外の背景（オーバーレイ）を半透明で表示できる。
`headerShown: false`はモーダルにヘッダーが必要ない場合は指定する。

```jsx
const nav = createStackNavigator();
export const ModalStackNav: React.FC = () => {
  return (
    <nav.Navigator>
      <nav.Screen 
        name="ExampleModal"
        component={ExampleModalScreen} 
        options={{
          cardOverlayEnabled: true,
          cardStyle: {
            marginTop: 100,
            marginBottom: 100,
            marginRight: 20,
            marginLeft:20,
            borderTopLeftRadius: 10,
            borderTopRightRadius: 10,
            borderBottomLeftRadius: 10,
            borderBottomRightRadius: 10
          },
          headerShown: false,
          presentation: 'transparentModal',
        }}
      />
    </nav.Navigator> 
  );
};
```

表示例は以下の通り。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1299342/beccbc4c-dbef-fdb6-5b1f-e8f80b0fc528.gif" width=20%>

## 背景をタップして遷移前の画面に戻る

StackNavigatorでモーダルを実現する場合、背景をタップして遷移前の画面に戻るためには、`onPress`したら`goBack`するようにコンポーネントを指定する。指定先の`option`として、`cardOverlay`がある。
`cardOverlay`を使用する場合は`cardOverlayEnabled: true`を必ず指定する（[ドキュメント](https://reactnavigation.org/docs/stack-navigator/#cardoverlay)）。

実装例を以下に示す。試行錯誤があった関係から適宜コメントを入れている。
※goBackの中身は趣旨から外れるため省略している。

```jsx
const nav = createStackNavigator();
export const ModalStackNav: React.FC = () => {
  return (
    <nav.Navigator>
      <nav.Screen
        name="ExampleModal"
        component={ExampleModalScreen} 
        options={{
          headerShown: false,
          presentation: 'transparentModal',
          cardOverlayEnabled: true,
          cardOverlay: () => 
            <>
              {/* 
                背景を透明にする。
                PressableでonPressを指定すればgoBackできそうだが、下記報告と同じような状況（2022.12.25時点確認）の場合onPressが効かない。
                https://github.com/react-navigation/react-navigation/issues/10114
                zIndexでモーダルの上に表示すればonPressが効くが、モーダルが透明の背景に覆われてしまう。
              */}
              <View
                style={[StyleSheet.absoluteFillObject, {
                  backgroundColor: 'rgba(0,0,0,0.7)',
                },]}
              />

              {/* 
                画面全体をタップ可能なコンポーネントで覆い、goBackできるようにする。
                背景色を指定するコンポーネントと別にPressableを配置することで上記問題を解決する。
              */}
              <Pressable
                style={[StyleSheet.absoluteFillObject,{ 
                  zIndex: 1,
                }]}
                onPress={goBack}
              />
              {/* 
                画面全体を覆うコンポーネント上に配置し、モーダルの大きさと一致させ、モーダル上でタップしてもgoBackしないようにする。
                cardStyleでzIndexを指定してもcardOverlayで指定しているコンポーネントより上に表示されないことへの対処。
              */}
              <View style={{
                marginTop:100,
                marginBottom: 100,
                marginRight: 20,
                marginLeft: 20,
                height: '80%',
                width: '90%',
                borderTopLeftRadius: 10,
                borderTopRightRadius: 10,
                borderBottomLeftRadius: 10,
                borderBottomRightRadius: 10,
                zIndex: 2,
              }}/>
            </>,
          cardStyle: {
            marginTop: 100,
            marginBottom: 100,
            marginRight: 20,
            marginLeft:20,
            height: '80%',
            width: '90%',
            borderTopLeftRadius: 10,
            borderTopRightRadius: 10,
            borderBottomLeftRadius: 10,
            borderBottomRightRadius: 10,
          },
        }}
      />
    </nav.Navigator> 
  );
};
```

[ドキュメント](https://reactnavigation.org/docs/stack-navigator/#transparent-modals)では背景をタップした場合はクローズしたり、ダイアログのアニメーションをカスタマイズしたい場合は`useCardAnimation`を使用すると案内されている。実際、StackNavigatorを使って実装しようとすると上記のように複雑になってしまうため、ドキュメントに従って実装した方がよさそうだ。

# StackNavigatorを使ってボトムシートを実装する

ボトムシートとは、画面の下辺からスライドアップして内容を表示する要素である。これをreact-navigationのモーダルで実現してみる。
`gestureEnabled: true`を指定するとモーダルを指で動かすことで閉じる動作ができる。
`...TransitionPresets.ModalPresentationIOS`を指定するとボトムシートと背景の見た目をより想定通りに実現できる。「IOS」と名前に入っているがandroidでも動作する。

```jsx
const nav = createStackNavigator();
export const ModalStackNav: React.FC = () => {
  return (
    <nav.Navigator>
      <nav.Screen 
        name="BottomSheet"
        component={BottomSheetScreen} 
        options={{
          cardStyle: {
            top: 100, 
            borderTopLeftRadius: 10,
            borderTopRightRadius: 10
          },
          headerShown: false,
          presentation: 'transparentModal',
          gestureEnabled: true,
          ...TransitionPresets.ModalPresentationIOS,
        }}
      />
    </nav.Navigator> 
  );
};
```

# あとがき

ここまでreact-navigationのモーダルの実装方法について調べてきた。
感想としては背景に遷移前の画面を表示するまでは容易に実装できたが、背景をタップしてクローズしたり、なんらかのアニメーションを加えたりしたい場合は実装難度が上がるため、より簡潔に実装するために実現方法の選択肢の幅を広く持っておく必要があると感じた。
React Nativeでは様々なライブラリがモーダル機能を提供しているので、仕様に合わせてどれが最適な実装方法かを吟味して選定する必要があると考える。

ちなみに、ここまで紹介したもの以外にもStackNavigatorのスクリーンオプションズにはmodalと名のつくpresentationの種類がある。興味がある場合は以下リンクから参照できる。
[Native Stack Navigator | React Navigation](https://reactnavigation.org/docs/native-stack-navigator/#presentation)