壁紙アプリに WidgetKit 拡張を足したくて、Rork から書き出した Expo プロジェクトを Xcode で開き、実機に Release 構成でインストールした直後のことでした。スプラッシュが消えた瞬間、画面いっぱいに赤い文字が出ます。
No bundle URL present.
Make sure you're running a packager server or have included a .jsbundle file in your application bundle.
開発ビルドでは問題なく動いていたので、最初は自分のコードを疑いました。けれど原因はコードではなく、「JavaScript の中身がアプリのどこにも見当たらない」という、もっと手前の話でした。同じ赤い画面を Debug ビルドで見たことのある方もいると思いますが、Debug と Release では出る理由がまったく違います。ここを取り違えると半日溶かします。私も実際に溶かしました。
このエラーが伝えていること
No bundle URL present は、ネイティブ側(iOS アプリの本体)が起動時に「どこから JavaScript を読み込めばいいのか分からない」状態です。React Native / Expo のアプリは、ネイティブの殻の中に JavaScript の塊(バンドル)を抱えて動きます。そのバンドルの在りかは2通りあります。
- Debug ビルド: Mac 上で動く Metro バンドラに
http://<MacのIP>:8081でつなぎ、その場で読み込む - Release ビルド: アプリの中に焼き込まれた
main.jsbundleというファイルから読み込む
赤い画面は「この2つのどちらも見つからなかった」というサインです。つまり原因の切り分けは、まず Debug で出たのか Release で出たのか から始まります。
Debug ビルドで出た場合 — Metro につながっていない
Xcode の構成が Debug のまま実機に入れたなら、ほぼ Metro 接続の問題です。私が遭遇したケースで多かった順に並べます。
- Metro を起動していない。 ターミナルで
npx expo startを立ち上げ忘れていただけ、というのが一番多い落とし穴です。 - Mac と iPhone が別の Wi-Fi にいる。 実機は
localhostでは Mac に届きません。同じネットワークに乗せるのが大前提です。 - 会社や自宅のルーターが端末間通信を遮断している。 ゲスト用 SSID やクライアント分離が有効な Wi-Fi だと、同じ SSID でも通信が通りません。
切り分けは単純で、iPhone の Safari で http://<MacのIP>:8081/status を開いてみてください。packager-status:running と返ってくれば経路は生きています。返ってこなければネットワーク側の問題です。
確実なのは、実機を USB でつないで次のコマンドで入れ直す方法です。
# 実機を USB 接続した状態で、ポートフォワードしつつ起動する
npx expo run:ios --deviceexpo run:ios は内部で xcrun devicectl などを使って端末を解決し、Metro へのポートも面倒を見てくれます。Wi-Fi が不安定な環境では、これが一番ストレスがありません。
Release ビルドで出た場合 — バンドルが焼き込まれていない
ここからが本題です。Release 構成では Metro につなぎません。アプリの中に main.jsbundle が入っていることが絶対条件で、それが無いと赤い画面になります。Rork から書き出したプロジェクトをローカルの Xcode でいじり始めた人が、ここで一番つまずきます。
原因は大きく2つです。
1. バンドル生成のビルドフェーズが走っていない
React Native の iOS プロジェクトには、ビルド時に JS をまとめる「Bundle React Native code and images」というシェルスクリプトのビルドフェーズがあります。Expo の prebuild で生成された素のプロジェクトには入っているはずですが、ネイティブを手で触っているうちにスキップ設定にしてしまったり、スクリプトの参照パスがずれていたりすると、Release でもバンドルが作られません。
Xcode で対象ターゲット → Build Phases を開き、Bundle React Native code and images フェーズが存在し、Run script only when installing のチェックが外れていることを確認してください。チェックが入っていると、通常の Run では走らず、Archive 時だけ走るため「実機に入れたら赤い画面、でも Archive は通る」という分かりにくい症状になります。
2. そもそも expo run:ios --configuration Release を使っていない
素の xcodebuild や Xcode の Run をそのまま Release で叩くより、Expo に Release ビルドを任せたほうが確実です。
# Expo にバンドル埋め込みごと Release ビルドを任せる
npx expo run:ios --configuration Release --deviceこのコマンドは内部でバンドルの生成とアプリへの同梱を済ませてから実機に入れてくれるので、手動のビルドフェーズ設定で悩む時間がぐっと減ります。
応急処置 — オフラインバンドルを手で作る
ビルドフェーズの設定をいじる前に「とにかく今すぐ実機で動かして確認したい」ときは、バンドルを手動で生成してプロジェクトに置く方法があります。
# ios ディレクトリ配下に main.jsbundle とアセットを生成する
npx react-native bundle \
--platform ios \
--dev false \
--entry-file index.js \
--bundle-output ios/main.jsbundle \
--assets-dest ios--entry-file は Expo Router を使っているプロジェクトでは node_modules/expo-router/entry.js になることがあります。エントリが見つからないと言われたら、package.json の main フィールドを確認してください。生成した main.jsbundle を Xcode のプロジェクトに「Create folder references」ではなくターゲットに追加しておけば、Release でもそこから読み込まれます。
ただしこれはあくまで応急処置です。手で置いたバンドルはコードを変えても自動更新されないため、原因(ビルドフェーズ)を直したら手動バンドルは外しておくのが安全です。何度かこの罠を踏んで、「動いたと思ったら古い JS が焼き込まれたままだった」という二次災害を起こしたので、ここは強めに書いておきます。
切り分けの順番(迷ったらこの順で)
赤い画面を見たら、私はいつもこの順で潰していきます。
- 構成は Debug か Release か(Xcode のスキーム左上で確認)
- Debug なら Metro が起動しているか、実機と Mac が同じ Wi-Fi か
- Release なら Build Phases にバンドル生成フェーズがあり、
only when installingが外れているか - それでも駄目なら
expo run:ios --configuration Release --deviceで焼き直す - 原因究明より先に動作確認を急ぐなら、オフラインバンドルを手動生成
この順を守るだけで、「コードを疑って延々ログを読む」という遠回りを避けられます。
予防 — 二度と半日溶かさないために
私は累計5,000万ダウンロードのアプリを2014年から個人で運用していますが、Rork で素早く骨格を作り、WidgetKit やライブアクティビティのようなネイティブ機能だけ Xcode で足す、という分業をよくします。そのとき毎回 Release で実機確認する癖をつけてから、このエラーで足を止めることはほぼ無くなりました。開発ビルドだけで「動いた」と判断せず、ストアに出す構成(Release)で一度は実機に入れておく。たったこれだけで、審査直前の青ざめる時間を防げます。
まず手元のプロジェクトを一度 npx expo run:ios --configuration Release --device で実機に入れてみてください。そこで赤い画面が出なければ、あなたの配布ビルドはバンドルをきちんと抱えています。同じところで止まっている方の参考になれば嬉しいです。