TestFlight で何十回もテストして「完璧だ」と確信してから App Store に公開した途端、クラッシュレポートが届く——これは Rork で iOS アプリを初めて公開した開発者の多くが通る洗礼です。
「TestFlight と App Store の違いは審査があるかどうかだけでしょう?」と思われる方もいるかもしれませんが、実は実行環境そのものにいくつかの重要な差異があります。その差異が本番クラッシュの原因になります。よく遭遇するパターンを4つに絞って、原因と修正手順を具体的にお伝えします。
なぜ TestFlight と App Store で挙動が変わるのか
本題に入る前に、理解しておくと後が楽になる前提知識があります。
TestFlight は 開発用・AdHoc 配布ビルド を配布する仕組みです。一方、App Store は 本番用(Distribution)証明書でビルドされたアーカイブが使われます。この証明書の違いに付随して、アプリが参照する設定ファイル、API キー、フラグ、ネットワーク設定などが変わる可能性があります。
また、TestFlight は「あなたやテスターが使っているデバイス」で動いていますが、App Store では数年前のモデル(iPhone 8 相当)や、OS バージョンが古い環境でも使われます。この2点を念頭に置いて、以下を読み進めてください。
パターン①:本番ビルドで環境変数が undefined になる
Rork で API を使うアプリを開発するとき、process.env.EXPO_PUBLIC_API_KEY のような環境変数を使うことがあります。TestFlight テスト中は .env ファイルや EAS の preview プロファイル設定で動いていても、App Store 向けの production プロファイルで別の環境変数を参照していた場合に、本番環境で変数が undefined になりクラッシュします。
確認すべき箇所
// eas.json — preview にしか env がないとproductionで undefined になる
{
"build": {
"preview": {
"distribution": "internal",
"env": {
"EXPO_PUBLIC_API_KEY": "test-key-here"
}
},
"production": {
"distribution": "store"
// ← env の定義がない!
}
}
}修正手順
- Expo ダッシュボード(expo.dev)→ プロジェクト → Secrets を開く
EXPO_PUBLIC_API_KEYを Environment: Production のシークレットとして追加eas.jsonのproduction.envで参照するよう修正する
"production": {
"distribution": "store",
"env": {
"EXPO_PUBLIC_API_KEY": "$EXPO_PUBLIC_API_KEY"
}
}eas build --platform ios --profile productionで再ビルドして動作確認
環境変数の設定ミスについては環境変数・設定ファイルのエラー対処ガイドも合わせて確認してください。
パターン②:App Transport Security(ATS)が本番で厳格に適用される
iOS には App Transport Security(ATS)という仕組みがあり、HTTP(非暗号化)通信をデフォルトでブロックします。TestFlight のデバッグビルドでは ATS の例外設定が緩くなっていても、本番ビルドでは厳格な ATS が適用されて通信が失敗するケースがあります。
症状で見分ける
- ネットワーク系のエラー(
NSURLErrorDomainやSSL関連)が Console に出る - 外部 API の呼び出しが全て失敗する
- アセットや画像の URL が
http://で始まっている
app.json での確認と修正
{
"ios": {
"infoPlist": {
"NSAppTransportSecurity": {
"NSAllowsArbitraryLoads": false,
"NSExceptionDomains": {
"your-api.example.com": {
"NSExceptionAllowsInsecureHTTPLoads": false,
"NSExceptionMinimumTLSVersion": "TLSv1.2"
}
}
}
}
}
}NSAllowsArbitraryLoads: true にすると審査で指摘を受ける可能性があるため、必要なドメインだけを例外設定するのが正しいやり方です。接続先 API サーバーが HTTPS に対応しているかどうかも再確認してください。
パターン③:プライバシーマニフェストの申告漏れ
2024年以降、Apple は Privacy Manifest(PrivacyInfo.xcprivacy) と Required Reason APIs の申告を必須化しました。TestFlight 配布では見逃されても、App Store の審査プロセスで「申告されていない API を使用している」として警告が届き、アプリが正常に動作しないケースがあります。
Rork で生成されたアプリが内部的に利用する可能性のある Required Reason APIs には以下のものがあります。
- UserDefaults(AsyncStorage 経由で内部利用)
- Disk space APIs(NSFileManager 経由)
- File timestamp APIs
Expo での対応
Expo SDK 50 以降では多くのマニフェスト設定が自動生成されますが、カスタムネイティブモジュールを使っている場合は手動対応が必要です。
{
"ios": {
"privacyManifests": {
"NSPrivacyAccessedAPITypes": [
{
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryUserDefaults",
"NSPrivacyAccessedAPITypeReasons": ["CA92.1"]
},
{
"NSPrivacyAccessedAPIType": "NSPrivacyAccessedAPICategoryDiskSpace",
"NSPrivacyAccessedAPITypeReasons": ["E174.1"]
}
]
}
}
}詳細な対応手順はプライバシーマニフェスト・Required Reason API 申告の修正ガイドを参照してください。
パターン④:低スペック端末でのメモリ不足クラッシュ
「iPhone 15 Pro Max で完璧に動いたから大丈夫」——この考え方が最も危険です。App Store では iPhone 8(2017年・RAM 2GB)のような端末でも動作することが期待されています。
AI 機能や画像処理を多用する Rork アプリでは、メモリ消費量が想定より大きくなりがちです。高スペック端末のテストでは問題なかったものが、メモリの少ない端末で OOM(Out of Memory)クラッシュを起こすことがあります。
対策:リスト表示の最適化
// FlatList の render 件数を制限してメモリを節約する
<FlatList
data={items}
renderItem={renderItem}
maxToRenderPerBatch={10} // 一度にレンダリングするアイテム数(デフォルト: 10)
windowSize={5} // 画面外を保持するウィンドウ数(デフォルト: 21)
removeClippedSubviews={true} // 非表示アイテムをアンマウントしてメモリを解放
initialNumToRender={8} // 初期レンダリング件数
/>対策:画像の適切なサイズ管理
import { Image } from 'expo-image'; // expo-image はキャッシュ管理が優秀
<Image
source={{ uri: imageUrl }}
style={{ width: 300, height: 300 }}
contentFit="cover"
// サムネイルには縮小URLを使い、フルサイズをメモリに展開しない
transition={200}
/>Rork のプロンプトに「メモリ効率を考慮した実装にしてください」「大きな画像は expo-image を使ってください」と明示すると、生成コードの質が上がることがあります。アプリ全体のパフォーマンス改善についてはパフォーマンス最適化ガイドも参考にしてください。
本番クラッシュを素早く把握するために Sentry を導入する
上記4パターンを確認してもクラッシュが再現しない場合、本番クラッシュのログを自分で確認できる環境を整える点が肝心です。
Sentry は Rork アプリへの導入が比較的シンプルで、クラッシュの発生箇所・スタックトレース・デバイス情報を自動収集してくれます。
npx expo install @sentry/react-native// app/_layout.tsx または App.tsx の最初に追加
import * as Sentry from "@sentry/react-native";
Sentry.init({
dsn: "https://your-dsn@sentry.io/project-id",
release: "com.yourcompany.yourapp@1.0.0",
dist: "1",
enabled: !__DEV__, // 本番環境のみ有効化
tracesSampleRate: 0.1,
});
export default Sentry.wrap(RootLayout);Sentry を入れておくと、次のリリース以降は「クラッシュがどのデバイス・どのコードで起きているか」が即座にわかります。設定の詳細はSentry クラッシュ監視のセットアップガイドをご覧ください。
App Store 公開前のチェックリスト
最後に、本番クラッシュを防ぐための確認項目をまとめます。
eas.jsonのproductionプロファイルに全ての環境変数が定義されているか- API 接続先が全て HTTPS に対応しているか
NSAllowsArbitraryLoadsをfalseのまま本番ビルドが通るか- Required Reason APIs が Privacy Manifest に全て申告されているか
- iPhone SE(第2世代以降)や iPhone 8 相当の端末で動作確認したか
FlatListや画像コンポーネントのメモリ使用量が適切か- Sentry または同等のクラッシュ監視ツールが本番環境に設定されているか
TestFlight では完璧に動いたのに本番でクラッシュする経験は、開発者なら誰でも一度は通る道です。ただ、原因のパターンは限られているので、この4つを頭に入れておけば対処が格段に早くなります。次のリリースが少しでもスムーズにいくことを願っています。