Open20

ReactNative知見

tenkeitenkei

ReactNativeを使って開発を始めたので、ここに知見を溜めていく。
ちなみに筆者はウェブ系。React歴は長いけど、ReactNativeは初めて。ネイティブアプリ開発自体が初めて。

tenkeitenkei

Unexpected text node エラー

React のノリで普通に開発していたら遭遇。
View の中にそのままテキストノード(文字列)をそのまま書いてはいけない。

// ❌️
const Name = () => {
    return (
        <View>Foo</View>
    )
}

// ✅️
const Name = () => {
    return (
        <View>
            <Text>Foo</Text>
        </View>
    )
}

Pressable の中も同様。テキストノードはとにかく Text で囲む。

tenkeitenkei

実機用の余白は SafeAreaView で対応

何も考えずに開発し、いざ実機で見てみると、余白の設定が間違っていることに気づく。
画面の上部が、実機の時刻表示やバッテリー表示部分と重なってしまっている。

この問題に対応するには、 SafeAreaView を使う。
https://reactnative.dev/docs/safeareaview

各スクリーンに適用する。コンポーネントツリーのルートを <View /> ではなく <SafeAreaView /> に置き換える。

style 指定をしつつ、必要な edges を指定する。

    <SafeAreaView style={styles.container} edges={["top", "left", "right"]}>
tenkeitenkei

複数のカスタムフォントを同時に指定できない?

ウェブ開発では、複数のカスタムフォントを利用することがある。CSS指定は以下のような感じ。

p {
    font-family: "MainFont, SubFont, DefaultFont";
}

左から順に評価され、マッチするフォントが適用される。例えば、英語では MainFont が適用され、日本語では SubFont が適用される、のような感じ。

しかし、ReactNative の <Text /> ではこの指定ができなかった。
単一の font-family 指定だと効いていのに、複数指定にしたらそもそもカスタムフォントが適用されなくなった。

調べてみても、そもそも複数のカスタムフォントを利用している例が少ない。挙句の果てには、システムフォントを使うのが無難ですという記事も見かけた。
というわけで、一般の文字はシステムフォントに任せることに…。装飾的な文字だけ個別にカスタムフォントを当てることにした。

tenkeitenkei

AndroidのためのCSS指定

AndroidのためのCSS指定がいくつかある。 elevation とか textVerticalAlign とか。逆にこれらを指定しないと、Android端末で微妙に見た目が異なってしまう。

tenkeitenkei

BottomTabNavigator でも SafeArea 考慮

BottomTabNavigator の下部が、実機のナビゲーションバーやインジケータと被っている。
こちらも、SafeArea の考慮を行う。

import { useSafeAreaInsets } from "react-native-safe-area-context";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";

const Tab = createBottomTabNavigator();

export const MainNavigator = () => {
  const insets = useSafeAreaInsets();

  return (
    <Tab.Navigator
      screenOptions={{
        tabBarStyle: {
          height: 55 + insets.bottom, 
          paddingTop: 4, // 上部の余白もあっていい
          paddingBottom: insets.bottom, // safe area 分の余白を設定
        },
      }}
    >
        {/* ここにタブ内容 */}
    </Tab.Navigator>
  );
};
tenkeitenkei

ReactNavigation のナビゲーション定義内では、あまり動的な記述はしないほうがいいかもしれない。

例えば以下のように定義する。

import { NavigationContainer } from "@react-navigation/native";

export function AppNavigator() {
  // Contextなどから認証情報を取る
  const { session } = useSession();

  return (
    <NavigationContainer>
      {/* 認証情報に応じて、出し分ける */}
      {session === null ? <AuthNavigator /> : <MainNavigator />}
    </NavigationContainer>
  );
}

すると、 AuthNavigatorMainNavigator の間では遷移ができなくなるっぽい。(たぶん)

Navigation 定義は以下のように静的にしておくのが良いかもしれない。こうすると、相互に遷移が可能になる。

import { NavigationContainer } from "@react-navigation/native";

export function AppNavigator() {
  return (
    <NavigationContainer>
      <AuthNavigator />
      <MainNavigator />
    </NavigationContainer>
  );
}
tenkeitenkei

Supabase Storage にファイルアップロードできない

Supabase Storage にファイルアップロードできない問題が発生。
2日くらいハマった。AIでも解決できず。

顛末について、以下に書いた。

https://zenn.dev/tenkei/articles/d29fb848808a1a

うーん、難しい。
localhostでの起動とExpoGoを含む実機での起動の環境差異がよく分かっていない。

tenkeitenkei

Previewビルドの待ち時間

Previewビルドを何度かしてみた。すぐに終わるときもあるし、数十分かかるときもある。
開発が一段落したタイミングで、ひとまずpreviewビルドしてみるのが良いのかも。とにかくキューに入れておく。

tenkeitenkei

KeyboardAvoidingView

OSのキーボードが表示されると、画面の下部が隠れてしまう。
ウェブだと画面全体が自動でスクロールアップされるが、ReactNativeだと自前で実装しないといけない。
以下のように KeyboardAvoidingView を利用する。

    <KeyboardAvoidingView 
      style={styles.container} 
      behavior={Platform.OS === "ios" ? "padding" : "height"}
      keyboardVerticalOffset={Platform.OS === "ios" ? 20 : 0}
    >
        // ここにスクロールアップさせたい要素
    </KeyboardAvoidingView>

https://reactnative.dev/docs/keyboardavoidingview

tenkeitenkei

スクロールには overflow:scroll ではなく ScrollView を使う

CSS の overflow:scroll している箇所が、Android 実機でスクロールできなかった。iOSでは未確認だけど、Claude によると同様っぽい。
ScrollView を使うことで解決。
というか、スクロールさせたい箇所は ScrollView や FlatView を使うべき。overflow:scroll を使う場面は無さそう。もはや linter で禁止にすべきかもしれない。

tenkeitenkei

ステータスバーのテキストが見えない

SafeAreaView を使うことで、アプリの内容がステータスバーと被らなくなった。ステータスバーとは、時刻表示やバッテリー表示の部分。
しかし、別の問題が発生。アプリの背景色が白で、ステータスバーの文字色も白なので、時刻表示などが見えない。
この問題は以下のように解決。

// app.tsx

import { StatusBar } from "expo-status-bar";

// レンダー部分
      <StatusBar style="dark" backgroundColor="#FFFFFF" translucent={false} />

dark を指定することで、文字色が黒になる。

tenkeitenkei

認証状態の保持

Supabase の認証状態を端末?に保持しておく方法。これをしないと、アプリを開くたびにサインインが必要になってしまう。

とは言え、SupabaseとReactNativeの組み合わせなら簡単。

まずは必要なパッケージのインストール。

npx expo install @react-native-async-storage/async-storage

次に、Supabase のクライアント初期化設定を修正する。

import { AppState } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createClient } from "@supabase/supabase-js";

import { Database } from "@/types/supabase";

const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;

export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
  // 以下の設定を追加!
  auth: {
    storage: AsyncStorage,
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: false,
  },
});

// こちらの設定も追加しておくと、勝手に認証状態のリフレッシュをしてくれる
AppState.addEventListener("change", (state) => {
  if (state === "active") {
    supabase.auth.startAutoRefresh();
  } else {
    supabase.auth.stopAutoRefresh();
  }
});

以上。

tenkeitenkei

カメラやファイルのアクセス権限

自分が作成しているアプリでは、カメラとファイルへのアクセスが必要。なので権限確認を実装する。
マイクや位置情報の権限は不要。

ちなみに、ここまでの開発では問題なかった。ローカル環境やプレビュービルドだと問題なかった。
しかしClaudeによると、本番ビルドでは問題になるらしい。
というわけで、最初から実装する必要はないけど、本番リリースの前にはやるべき事項だ。

tenkeitenkei

expo-image-picker vs expo-camera

ReactNative でカメラ機能を使う場合、expo-image-picker と expo-camera の2つの選択肢がある。
前者はシンプルで、後者は複雑なことまでできるらしい。
シンプルな用途の場合、expo-image-picker で十分っぽい。

tenkeitenkei

本番ビルド確認

アプリの公開前に本番ビルドを試しておく。
プレビュービルドまでは問題なかったのに、本番ビルドでは落ちた!みたいなことはあるらしい。

本番ビルドの流れについて、以下の記事に書いた。1発で成功して良かった…!

https://zenn.dev/tenkei/articles/ae7a964da85583

tenkeitenkei

アプリのバージョン更新

app.json で versionCode を変更しても Expo 上で反映されなかった。

これは esa.json で cli.appVersionSource: "remote" となっているためだった。新しいバージョンでは、remote が標準っぽい?
このとき、app.json の versionCode は無視される。

esa.json で build.production.autoIncrement: true を指定することで解決。

公式ドキュメントのこのあたりに書いてある。
https://docs.expo.dev/build-reference/app-versions/

tenkeitenkei

音声再生

expo-audio パッケージを使う。 expo-av パッケージは deprecated なので注意。