Rork が生成したアプリの「画像を選ぶ」ボタンを押しても、シミュレータでは反応するのに、TestFlight のビルドだとボタンを押しても何も起こらありません。先日、ちょうどそのパターンで丸一日溶かしました。
私は2014年から壁紙系のアプリを中心に個人開発を続けていて、累計5,000万DLを超えるアプリ群の中でも画像ピッカーは10本以上のアプリで使っています。それでも、Rork の AI が出してくる expo-image-picker のコードをそのまま信じて Info.plist の存在を忘れていると、毎回同じ場所で止まります。原因は概ね4つに集約できますので、上から順に潰していくと多くのケースで動くようになります。
原因1: Info.plist のキーが入っていない(iOS で最初に詰まる場所)
iOS 10 以降、フォトライブラリやカメラへのアクセスには Info.plist に「使用理由の文字列」が必須です。Expo / Rork で生成されたアプリでも、最初にビルドした段階ではキー自体が入っていないことが普通にあります。
詰まり方の特徴は、シミュレータでは動くのに実機(特に TestFlight 経由のビルド)でボタンを押すと無反応、もしくはアプリがそのままクラッシュするというものです。
app.json に以下のように infoPlist を書き足します。
{
"expo": {
"ios": {
"infoPlist": {
"NSPhotoLibraryUsageDescription": "プロフィール画像を選ぶためにフォトライブラリへのアクセスを許可してください。",
"NSCameraUsageDescription": "プロフィール写真を撮影するためにカメラへのアクセスを許可してください。",
"NSPhotoLibraryAddUsageDescription": "アプリで作成した画像をフォトライブラリへ保存するために必要です。"
}
}
}
}書き換えただけでは反映されません。Expo Go なら開発サーバ再起動、ネイティブビルドの場合は EAS Build でビルドし直す必要があります。「app.json を変えたのに変わらない」という相談をいただくとき、9割はビルドし直していないだけ、というのが私の経験則です。
原因2: 権限を先に取らずに launchImageLibraryAsync を呼んでいる
Rork が生成するコードでよく見かけるのが、権限ダイアログを出さずにいきなり launchImageLibraryAsync を呼ぶパターンです。プラットフォームによっては「権限がないので無言で結果なしを返す」挙動になり、ボタンを押しても何も起きないように見えます。
正しい呼び出し順は次の通りです。
import * as ImagePicker from 'expo-image-picker';
import { Alert, Linking } from 'react-native';
async function pickImage() {
// 1. 先に権限を要求する(拒否時はガイドを出して return)
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
Alert.alert(
'写真へのアクセスが許可されていません',
'設定アプリから写真へのアクセスを許可してください。',
[
{ text: 'キャンセル', style: 'cancel' },
{ text: '設定を開く', onPress: () => Linking.openSettings() },
]
);
return;
}
// 2. 権限が取れたあとでピッカーを起動する
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 0.8,
});
if (!result.canceled) {
// 期待する出力: result.assets[0].uri に file:// から始まる画像のURIが入る
console.log(result.assets[0].uri);
}
}特に重要なのは、権限が拒否されたときに「設定アプリへ誘導するダイアログを出す」ことです。一度「許可しない」を選んだユーザーは、その後二度とOSの権限ダイアログが出てこないので、アプリ側で道筋を示してあげる必要があります。これを忘れると「ボタンを押しても何も起こらない」というレビュー☆1がそのままアプリに刻まれます。私の壁紙アプリでも、AdMob の月収ピーク時にこのレビューが付いたことが何度かあり、対応漏れの怖さを身に染みて学びました。
原因3: iOS 14 以降の Limited Photo Access の罠
iOS 14 で「写真の一部だけ許可する」モード(Limited Photo Access)が追加されました。ユーザーが「選択した写真のみ」を選ぶと、アプリ側からは許可された写真だけしか見えません。
requestMediaLibraryPermissionsAsync() は Limited のときも granted を返すので、コード上は「許可された」ことになります。ただし、ユーザー側から見ると「写真がほとんど表示されない」状態になります。これに気付かず「画像ピッカーが壊れた」と判定して問い合わせが来るパターンを、私自身も何度か経験しました。
Limited のときに「すべての写真へのアクセスに変更する」ためのフローを追加したい場合は、iOS のネイティブ API である PHPhotoLibrary.shared().presentLimitedLibraryPicker(from:) を呼ぶ必要があります。Expo の純正モジュールでは抽象化されていないため、個人開発の現場では「ユーザーが Limited を選んでもアプリの体験が崩れない設計」にする方が現実的だと考えています。
具体的には次のような対応が安全です。
- 結果が空でも「写真が選択されていません」と説明文を出す
- 設定画面から「すべての写真」へ変更する手順をスクショ付きで案内する
- アプリのオンボーディングで「すべての写真」を推奨する文言を入れる
権限まわりの設計の根本的な考え方については、Rork のアプリで権限ダイアログが表示されないときの確認ポイント も合わせて参考になります。
原因4: Android 13+ のメディアアクセス権限の変化
Android 12 までは READ_EXTERNAL_STORAGE で写真にアクセスできていましたが、Android 13(API レベル 33)以降は READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / READ_MEDIA_AUDIO に分割されました。
Expo SDK 49 以降の expo-image-picker は内部的に新しい権限へ追従してくれますが、app.json の android.permissions を旧いまま書いているとビルド段階で警告が出たり、Play Store の審査で「不要な権限を要求している」と指摘されたりします。
{
"expo": {
"android": {
"permissions": [
"android.permission.READ_MEDIA_IMAGES",
"android.permission.READ_MEDIA_VIDEO"
]
}
}
}注意したいのは、Android 13+ で導入された「Photo Picker」(システム提供のピッカー)を使うとそもそもこれらの権限が不要になる点です。Expo の expo-image-picker は Android 13+ で自動的に Photo Picker を使う設計になっているので、シンプルなプロフィール写真の選択だけなら権限要求を完全に省略できます。launchImageLibraryAsync の呼び出しだけで動くため、起動しないと感じたときは「そもそも権限要求が走っていない」可能性も視野に入れていただきたいところです。
Expo のネイティブビルド全体の構造を見直したいときは Expo の prebuild とカスタムネイティブモジュールの取り扱い と Rork で環境変数を扱うときの設定エラー対処 もあわせて確認しておくと、「Info.plist だけの問題ではなかった」と後から気付くケースを減らせます。
まずやること1つだけ
長くなりましたが、画像ピッカーが起動しないと感じたら、最初にやるべきは「app.json の ios.infoPlist に NSPhotoLibraryUsageDescription を書き足してから EAS Build をかけ直す」ことです。私自身、もう何度この一手で解決してきたか分かりません。
権限ダイアログ周りの設計は、React Native の入門書では薄く扱われがちな領域です。
同じ場所で詰まっている方の作業時間が、少しでも短くなれば嬉しいです。お読みいただきありがとうございました。