0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ReScriptでreason-react-navigationのTabNavigationを動かす

Posted at

要約

ReasonMLやReScriptは参考資料が少なく、実装で困った時の対応が難しい。
今回は、ReScript x Reason React Nativeの環境下で、reason-react-navigationのライブラリの仕様を追いつつ、TabNavigationを動かしてみた。

reason-react-navigationを利用した既存実装

reason-react-navigationのexampleではStackNavigatorを用いてmodal画面を出す実装が書かれている。

reason-react-naigation-exampleではDrawerNavigatorとTabを組み合わせている。これを基準にすればそれなりのアプリはできそうである。

reason-react-naigation-example

今回はTabNavigatorを利用して、アプリの骨組みを作る。

エディタ設定

ReasonML/ReScript環境向けのプラグインがあるので、入れておく。
ReScript Docs / Language Manual / EditorPlugins

初期設定

Reason React Nativeの公式ドキュメント "Create a new project with Reason React Native"に従い、コマンドを実行する。

npx @react-native-community/cli init --template @reason-react-native/template <your-project-name>

するとプロジェクトに必要なファイル一式が作成される。
プロジェクトのディレクトリに移り、npmのパッケージ群をインストールする。

cd <your-project-name>
yarn insyall

.re/.resファイルをbuild、あるいはwatchする。そして、通常のreact-native同様、run-ios/run-androidを実行する。
コマンドはpackage.jsonscriptsの項で確認できる。

package.json(summarized)
"scripts": {
  "start": "react-native start",
  "ios": "react-native run-ios",
  "android": "react-native run-android",
  "re:clean": "bsb -clean-world",
  "re:watch": "bsb -clean-world -make-world -w",
  "re:build": "bsb -clean-world -make-world",
  "test": "jest"
}
yarn re:watch

でbuildを行い、.bs.js形式ファイルを出力し、ファイルの変更があった場合に自動で再出力する。

別窓でreact-nativeを走らせる。iOSの場合はpod installが必要。

(iosの場合)
yarn ios

.re -> .res

この手順は行わなくても良い。ただ、App.reは削除する必要がある。

デフォルトのテンプレートはReasonMLで書かれているが、今回はReScriptで記述するので、変換してみる。
ReasonMLのファイル(.re)からReScriptのファイル(.res)はコマンド一つで変換する事ができる。

node_modules/.bin/bsc -format src/App.re > App.res

元のファイルが残るため、このまま起動するとバグるので注意。古いファイルは消す。

動かすとReason React NativeのReasonMLの際と同じ画面が見られる。

Reason React Native Default

目標構造

Javascript実装のサンプルの2例を組み合わせると以下のようになっている。
これを元にReScriptを書いていく。

import * as React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

// You can import Ionicons from @expo/vector-icons/Ionicons if you use Expo or
// react-native-vector-icons/Ionicons otherwise.
import Ionicons from 'react-native-vector-icons/Ionicons';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

const Tab = createBottomTabNavigator();

export default function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            let iconName;

            if (route.name === 'Home') {
              iconName = focused
                ? 'ios-information-circle'
                : 'ios-information-circle-outline';
            } else if (route.name === 'Settings') {
              iconName = focused ? 'ios-list-box' : 'ios-list';
            }

            // You can return any component that you like here!
            return <Ionicons name={iconName} size={size} color={color} />;
          },
        })}
        tabBarOptions={{
          activeTintColor: 'tomato',
          inactiveTintColor: 'gray',
        }}
      >
        <Tab.Screen name="Home" component={HomeScreen} />
        <Tab.Screen name="Settings" component={SettingsScreen} />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

Tab実装

まず、単純なTab Navigationを実装する。

package

reason-react-navigationとそれに該当するライブラリをインストールする

yarn add @react-navigation/bottom-tabs @react-navigation/native react-native-screens react-native-safe-area-context reason-react-navigation

bsconfig.jsonreason-react-navigationを追加する

bsconfig.json(bs-dependencies)
"bs-dependencies": [
  "reason-react",
  "reason-react-native",
  "reason-react-navigation"
]

ディレクトリ

src
├── App.res
├── RootNavigator.res
└── screens
    ├── Home.res
    ├── Second.res
    └── Third.res
Home.res
open ReactNative

@react.component
let make = (~navigation, ~route) => <SafeAreaView><Text> {React.string("home")} </Text></SafeAreaView>

Home.res, Second.res, Third.resは同様に作成、ファイル名と文字列だけ変更したものを用意する。

RootNavigator.res
open ReactNavigation

module RootNavigator = {
    include BottomTabs.Make({
        type params = unit;
    })

    @react.component
    let make = () => <>
        <Native.NavigationContainer>
            <Navigator>
                <Screen name="main" component=Home.make />
                <Screen name="second" component=Second.make />
                <Screen name="third" component=Third.make />
            </Navigator>
        </Native.NavigationContainer>
    </>
}

App.res
open RootNavigator

@react.component
let app = () => <RootNavigator />

カスタマイズ

tabBarOptions

Tab.NavigationtabBarOptionsを記述する。型で指定されているので、オブジェクトを入れても通らない。

Tab.NavigatorのPropsは

reason-react-navigation/src/BottomTabs.re(navigatorProps)
type navigatorProps = {
  initialRouteName: option(string),
  screenOptions: option(optionsCallback),
  backBehavior: option(string),
  _lazy: option(bool),
  tabBar: option(Js.t(bottomTabBarProps) => React.element),
  tabBarOptions: option(bottomTabBarOptions),
};

と定義されていて、tabBarOptionsが該当する。
続いて、型として指定されているbottomTabBarOptionsを調べる。

reason-react-navigation/src/BottomTabs.re(bottomTabBarOptions)
[@bs.obj]
external bottomTabBarOptions:
  (
    ~keyboardHidesTabBar: bool=?,
    ~activeTintColor: string=?,
    ~inactiveTintColor: string=?,
    ~activeBackgroundColor: string=?,
    ~inactiveBackgroundColor: string=?,
    ~allowFontScaling: bool=?,
    ~showLabel: bool=?,
    ~showIcon: bool=?,
    ~labelStyle: ReactNative.Style.t=?,
    ~tabStyle: ReactNative.Style.t=?,
    ~labelPosition: labelPositionArgs => string=?,
    ~adaptive: bool=?,
    ~style: ReactNative.Style.t=?,
    unit
  ) =>
  bottomTabBarOptions;

~(チルダ)はラベル付き引数を表すらしい。
unitとは何ぞや?という気持ちになるが、
ReasonMLを使う理由…ではなく入門によれば、

OCamlにおける経験則として、引数にunit()を配置することをとすることで、
その位置までの引数を無視する、つまりNoneを指定して関数を実行することを明示できます。

ということらしい。

RootNavigator.res(tabBarOptions)
let tabBarOptions = bottomTabBarOptions(
    ~activeTintColor="tomato",
    ~inactiveTintColor="gray",
    (),
)

とし、

RootNavigator.res(tabBarOptionsAdded)
@react.component
let make = () => <>
    <Native.NavigationContainer>
        <Navigator
            tabBarOptions
        >
            <Screen name="main" component=Home.make />
            <Screen name="second" component=Second.make />
            <Screen name="third" component=Third.make />
        </Navigator>
    </Native.NavigationContainer>
</>

とすれば、

<Tab.Navigator
    tabBarOptions={{
      activeTintColor: 'tomato',
      inactiveTintColor: 'gray',
    }}
>
</Tab.Navigator>

と同等になる。なお、JavaScriptではタグ内のプロパティ単独表記はプロパティにtrueが代入されていたが、ReScriptではプロパティと同名の変数の値が入る。JavaScriptのオブジェクト作成時と同じ感覚。
ReScript Docs / Language Manual / Overview #JSX

use tabBarOptions

screenOptions-Ionic

次にscreenOptionsにとりかかる。

screenOptionsの型

Tab.NavigatorのProps

reason-react-navigation/src/BottomTabs.re(navigatorProps)
type navigatorProps = {
  initialRouteName: option(string),
  screenOptions: option(optionsCallback),
  backBehavior: option(string),
  _lazy: option(bool),
  tabBar: option(Js.t(bottomTabBarProps) => React.element),
  tabBarOptions: option(bottomTabBarOptions),
};

より、screenOptionsの型はoption(optionsCallback)

optionsCallbackの型は

reason-react-navigation/src/BottomTabs.re(optionsCallback)
type optionsCallback = optionsProps => options;

で、 optionsProps, optionsの型は

reason-react-navigation/src/BottomTabs.re(optionsProps)
type optionsProps = {
  navigation,
  route,
};

reason-react-navigation/src/BottomTabs.re(options)
[@bs.obj]
external options:
  (
    ~title: string=?,
    //TODO: dynamic, missing static option: React.ReactNode
    ~tabBarLabel: tabBarLabelArgs => React.element=?,
    ~tabBarIcon: tabBarIconArgs => React.element=?,
    ~tabBarAccessibilityLabel: string=?,
    ~tabBarTestID: string=?,
    ~tabBarVisible: bool=?,
    ~tabBarButton: React.element=?,
    ~unmountOnBlur: bool=?,
    unit
  ) =>
  options;

となる。今回用いるtabBarIconの型tabBarIconArgs => React.element=?の引数tabBarIconArgsの型は

reason-react-navigation/src/BottomTabs.re(options)
type tabBarIconArgs = {
  focused: bool,
  color: string,
  size: float,
};

となっている。

これらに基づいて組み立てていく。大枠は以下。

let screenOptions = ({navigation, route}: optionsProps) => {

    @react.component
    let selecter = ({focused, color, size}: tabBarIconArgs): React.element => {
     // 何らかの処理
        <>
          // アイコンのコンポーネント
        </>
    }

    options(
        ~tabBarIcon=selecter,
        (),
    )
}

フォント

今回はreact-native-vector-iconsのIonicを使う。

まず、yarnでインストールし、

yarn add react-native-vector-icons bs-react-native-vector-icons

bsconfig.jsonbs-react-native-vector-iconsを加える。

bsconfig.json(bs-dependencies)
"bs-dependencies": [
  "reason-react",
  "reason-react-native",
  "reason-react-navigation",
  "bs-react-native-vector-icons"
]

また、Installationで言及されている通り、各OS毎にフォントを利用できるようにするための手順があるので、それに従いプロジェクトファイルに変更を加える。

RNIcons.re(Ionicons)
module Ionicons = {
  [@bs.deriving jsConverter]
  type name = [
    | [@bs.as "ios-add"] `_iosAdd
    | [@bs.as "ios-add-circle"] `_iosAddCircle
    // (中略)
    | [@bs.as "md-woman"] `_mdWoman
  ];
  let nameToJs = nameToJs;

  [@bs.module "react-native-vector-icons/Ionicons"] [@react.component]
  external make:
    (~name: string, ~color: string=?, ~size: float=?) => React.element =
    "default";

  let makeProps = (~name) => makeProps(~name=nameToJs(name));
};

ReasonML/ReScriptに詳しくなく、ライブラリに使い方が書いてなくて困った。

Ionicons.make({"name":"ios-home", "color":Some(color), "size":Some(size)})

で型を合わせたつもりが、Cannot call a class as a functionエラーで動かず。

<Ionicons name color size />

nameに文字列を入れるも動かず。

何時間もわからんといいながらたどり着いたのが、類似のライブラリreason-expo-vector-iconsのページ。

<Ionicons name=`md-checkmark-circle size=32 color="green" />

なるほど。`hogeがENUMみたいなやつなのね、と。(多相variantというらしい

この例のように、RNIcons.reに存在する`_iosHomeをnameに入れてみる

<Ionicons name=`_iosHome color size />

残念ながら動かない。だってこのコードはReasonMLじゃなくてReScriptだから。
ReScript Docs / Language Manual / MigrateFromBucklescriptReason

Polymorphic variants: from `red to #red.

多相variantの指定方法は`redから#redに変更。なるほど。

<Ionicons name=#_iosHome color size />

でFA。

あとはroute.nameでパターンマッチを行い、タブによるアイコンの出し分けを行う。

RootNavigator.res(screenOptions)
let screenOptions = ({route}: optionsProps) => {

    @react.component
    let selecter = ({color, size}: tabBarIconArgs): React.element => {
        let name = switch route.name {
            | "main" => #_iosHome
            | "second" => #_iosList
            | "third" => #_iosHeart
            | _ => #_iosInformationCircle
        }
        <Ionicons name color size />
    }

    options(
        ~tabBarIcon=selecter,
        (),
    )
}

NavigatorにもscreenOptionsを加えてやる。

RootNavigator.res(tabBarOptionsAdded)
@react.component
let make = () => <>
    <Native.NavigationContainer>
        <Navigator
            screenOptions
            tabBarOptions
        >
            <Screen name="main" component=Home.make />
            <Screen name="second" component=Second.make />
            <Screen name="third" component=Third.make />
        </Navigator>
    </Native.NavigationContainer>
</>

result

無事Tabにアイコンが表示された。
めでたし。めでたし。

実装結果

GitHubに上げてみた。iOS Simulatorだけで試してたので、Androidで動かす場合はフォントの設定が必要。
rescript-react-navigation-tab-sample - GitHub

感想

仕事でReactNativeを使ったりしていて、これまで書いていた最新のECMAScript準拠のJavaScriptに代わる言語として、TypeScriptよりはReScriptを使いたい気持ちがあるが、記事やサンプルコードがほとんど転がっていないので、ハマったときの時間のロスが大きいなぁ、という気持ちになった。なかなかサポートがない状態ではこの言語で業務開発は難しそう。チーム開発では自分のトラブルシュートに加え、仲間のサポートもしなくてはいけなくなるし。美しく書けるというのは良いのだが、納期も予算もあるんだよ…。

あと、最初にReScriptの説明書こうと思ったけど何も思いつかなくてやめた。BuckleScript+ReasonML=ReScript? ReasonMLの時も文法変わってた気がするし、BuckleScriptを書いたことがないからわからない…。

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?