◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Expo SDK 58 が 10/15 に正式リリースされました(Beta は 09/15 から)。上げる前に依存の対応状況を確認しましょう●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Expo SDK 58 が 10/15 に正式リリースされました(Beta は 09/15 から)。上げる前に依存の対応状況を確認しましょう●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/Rork入門
◈ Rork入門/2026-10-19中級

plugins に何も書いていないのに、Info.plist に権限の説明文が 9 件並んでいました

app.json の plugins が空でも、入れたパッケージの分だけ Info.plist に権限の説明文が出ます。Expo SDK 58 で prebuild して数え、外す設定と点検スクリプトを実測つきでまとめます。

Expo221Info.plist7prebuild4iOS 権限Android 権限リリース前チェックRork591

app.json の plugins は空のままで、位置情報の許可ダイアログを出すコードも、まだ書いていない段階でした。そこで念のため、iOS のプロジェクトを書き出して Info.plist を開いてみました。

そこにあったのは、カメラ、連絡先、マイク、モーション、写真ライブラリの説明文でした。どれも「Allow $(PRODUCT_NAME) to access your camera」のような英語の既定文のままです。

AI にアプリを作ってもらうと、機能を足すたびにパッケージが増えていきます。そのたびに、使い道のない権限の説明文もこっそり増えているのではないか。その数を、実際に数えてみました。

測ったこと

検証用の空のプロジェクトを作り、次のパッケージを入れました。Expo SDK 58 の版で、npx expo install が選んだものです。

パッケージ版
expo58.0.14
react-native0.88.0
expo-location58.0.17
expo-camera58.0.10
expo-contacts58.0.7
expo-media-library58.0.10
expo-image-picker58.0.10

app.json は名前とバンドル ID だけにして、plugins は書きませんでした。そこで次のコマンドを実行します。Linux 上で動かしたため、pod install を飛ばす --no-install を付けています。

CI=1 npx expo prebuild --platform ios --no-install
grep -oE "<key>NS[A-Za-z]+UsageDescription</key>" ios/probe/Info.plist

結果: 説明文は 9 件、すべて英語の既定文でした

キー由来(外したときに消えた設定)
NSCameraUsageDescriptionexpo-camera
NSContactsUsageDescriptionexpo-contacts(contactsPermission: false で消えました)
NSLocationWhenInUseUsageDescriptionexpo-location
NSLocationAlwaysUsageDescriptionexpo-location
NSLocationAlwaysAndWhenInUseUsageDescriptionexpo-location
NSMotionUsageDescriptionexpo-location(プラグインのソースに記述があります)
NSMicrophoneUsageDescriptionexpo-camera と expo-image-picker(両方で microphonePermission: false にすると消えました)
NSPhotoLibraryUsageDescriptionexpo-image-picker と expo-media-library
NSPhotoLibraryAddUsageDescriptionexpo-media-library(savePhotosPermission: false で消えました)

意外だったのは、位置情報だけを使うつもりのアプリでも、位置情報のパッケージ一つで「常に許可」「モーション」を含む 4 件の説明文が入ったことです。plugins に expo-location を書かなくても同じでした。入れたパッケージの既定値が、書かなくても反映されます。

仕組みの細部までは追っていません。ただ、plugins を空にした場合と、5 つの名前を並べた場合で、結果は同じでした。パッケージを expo-location だけに減らして再実行すると、鍵は 4 件の説明文と、精度の一時許可を示す辞書キー(NSLocationTemporaryUsageDescriptionDictionary)の合計 5 件になりました。

外す設定は、パッケージごとに名前が違います

位置情報なら、次のように書きます。使うのは「アプリの使用中だけ」の許可で、文面は自分の言葉にします。

{
  "expo": {
    "plugins": [
      [
        "expo-location",
        {
          "locationWhenInUsePermission": "近くの店舗を表示するために、現在地を使います。",
          "locationAlwaysPermission": false,
          "locationAlwaysAndWhenInUsePermission": false,
          "motionUsagePermission": false
        }
      ]
    ]
  }
}

この設定で prebuild し直すと、位置情報に関する鍵は NSLocationWhenInUseUsageDescription の 1 件になり、「常に許可」とモーションの説明文は消えました。精度の一時許可の辞書キーは残ります。プラグインのソースを読むと locationFullAccuracyPermission: false で入らなくなるようですが、こちらは実行して確かめていません。

複数のパッケージを入れている場合は、一つだけ止めても別のパッケージが同じ鍵を足します。私の検証でも、expo-image-picker のカメラを止めたのに NSCameraUsageDescription が残りました。expo-camera 側の既定が効いていたためです。

もう一つの方法は、使っていないパッケージを外すことです。使わない機能のために入れたパッケージは、権限と一緒に消すのが一番確実です。

Android は別の書き方です

同じ空のプロジェクトで Android を書き出すと、uses-permission が 15 件並びました。連絡先の読み書き、録音、外部ストレージ、画面上への重ね表示などが含まれます。

外すときは app.json の android.blockedPermissions に書きます。連絡先の 2 件と録音を指定して prebuild し直したところ、3 件とも tools:node="remove" が付いた形でマニフェストに残りました。マージ時に取り除く指定で、消える仕組みです。

{
  "expo": {
    "android": {
      "blockedPermissions": [
        "android.permission.READ_CONTACTS",
        "android.permission.WRITE_CONTACTS",
        "android.permission.RECORD_AUDIO"
      ]
    }
  }
}

数えるスクリプト

目で見て数えるのは、続きません。書き出した Info.plist を読み、自分が使う権限の一覧と照らし合わせるスクリプトを書きました。依存パッケージはなく、Node だけで動きます。

// plist-audit.mjs
// 使い方: node plist-audit.mjs ios/<アプリ名>/Info.plist used-permissions.json
import { readFileSync } from "node:fs";
 
const [plistPath, usedPath] = process.argv.slice(2);
if (!plistPath || !usedPath) {
  console.error("usage: node plist-audit.mjs <Info.plist> <used-permissions.json>");
  process.exit(2);
}
 
const xml = readFileSync(plistPath, "utf8");
// <key>NSxxxUsageDescription</key> の直後の <string> だけを拾う(辞書型は対象外)
const re = /<key>(NS\w+UsageDescription)<\/key>\s*<string>([^<]*)<\/string>/g;
const found = [...xml.matchAll(re)].map((m) => ({ key: m[1], text: m[2] }));
 
// 自分が使う権限だけを書いたファイル。例: ["NSLocationWhenInUseUsageDescription"]
const used = new Set(JSON.parse(readFileSync(usedPath, "utf8")));
const isDefault = (t) => /^Allow \$\(PRODUCT_NAME\)/.test(t);
 
let bad = 0;
for (const { key, text } of found) {
  const flags = [];
  if (!used.has(key)) flags.push("未使用の可能性");
  if (isDefault(text)) flags.push("既定の英語文のまま");
  if (flags.length) bad++;
  console.log(`${flags.length ? "NG" : "OK"}  ${key}${flags.length ? "  <- " + flags.join(" / ") : ""}`);
}
for (const k of used) {
  if (!found.some((f) => f.key === k)) {
    console.log(`??  ${k}  <- 使うと書いたのに Info.plist に無い(実行時に落ちる可能性)`);
  }
}
console.log(`\n${found.length} 件中 ${bad} 件を要確認`);
process.exit(bad ? 1 : 0);

used-permissions.json には、アプリで本当に使う権限の鍵だけを書きます。位置情報(使用中のみ)だけを使うなら、次の 1 行です。

["NSLocationWhenInUseUsageDescription"]

最初の状態(plugins が空で 5 つのパッケージを入れた状態)に流すと、次のとおりでした。

NG  NSCameraUsageDescription  <- 未使用の可能性 / 既定の英語文のまま
NG  NSContactsUsageDescription  <- 未使用の可能性 / 既定の英語文のまま
NG  NSLocationAlwaysAndWhenInUseUsageDescription  <- 未使用の可能性 / 既定の英語文のまま
NG  NSLocationAlwaysUsageDescription  <- 未使用の可能性 / 既定の英語文のまま
NG  NSLocationWhenInUseUsageDescription  <- 既定の英語文のまま
NG  NSMicrophoneUsageDescription  <- 未使用の可能性 / 既定の英語文のまま
NG  NSMotionUsageDescription  <- 未使用の可能性 / 既定の英語文のまま
NG  NSPhotoLibraryAddUsageDescription  <- 未使用の可能性 / 既定の英語文のまま
NG  NSPhotoLibraryUsageDescription  <- 未使用の可能性 / 既定の英語文のまま
 
9 件中 9 件を要確認

位置情報だけに絞り、説明文を日本語に直した設定では、OK が 1 件で終わりました。終了コードが 0 になるので、リリース前の確認手順に組み込めます。

消す前に、一つだけ確かめます

この点検は、外してよい権限と、外すと壊れる権限の区別までは付けてくれません。?? の行は、そのためにあります。

iOS では、権限の説明文が無いまま、該当の機能(カメラ、位置情報など)を呼び出すと、アプリが終了します。「使っていない」と思っていた機能が、実は別の画面から呼ばれているかもしれません。

私は次の順で進めます。

  1. コードの中で、カメラ・連絡先・位置情報などを呼んでいる箇所を検索します。
  2. 呼んでいない権限を used-permissions.json に入れないまま、スクリプトを走らせます。
  3. NG の鍵について、パッケージごと外すか、false で止めるかを決めます。
  4. 残す鍵の説明文を、その画面で何に使うのかが分かる自分の言葉に書き直します。

App Store 審査ガイドラインの 5.1.1 は、権限を求めるときに目的を明確に伝えることを求めています。説明できない権限の文面が並んだまま提出すると、あとで自分も説明に困ります。審査でどう扱われるかは、私には分かりません。ただ、**説明文は「入ってくるもの」ではなく、「残すと決めたものだけを置く場所」**だと考えるようにしました。

なお、この検証は Expo の iOS プロジェクトでの結果です。Rork Max が生成する SwiftUI のプロジェクトは、権限の入り方が異なります。パッケージの版が変われば、既定の鍵の数も変わるはずです。今回の数字は、上の版の組み合わせで得た値としてお読みください。

書き出し直しでネイティブ側の設定がどう動くかは、prebuild --clean で消える手入れを、上げる前に数える — Expo SDK 57 移行の棚卸し に整理しています。ビルドした後のアプリの中身を調べる方法は、リリース前に、書き出した Expo バンドルの中から自分の鍵を探してみる で扱っています。Apple から Privacy Manifest について指摘を受けた場合は、Rork で出したアプリが ITMS-91053 で弾かれたら — Privacy Manifest と Required Reason API の実務 もあわせてご覧ください。

次にリリースを出す前に、prebuild を一度回して Info.plist を開いてみていただければ幸いです。数は、思っていたより多いかもしれません。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

◈ Rork 入門2026-10-15
ストア審査を待つあいだ、Web 版を先に公開してよいか——Rork で作ったアプリの出し方を3つの条件で決める
Rork で作ったアプリを、Web で先に配るか、最初からストアに出すか。Rork と Expo の公式ページで確かめた事実をもとに、Rork Pro と Rork Max で選べる道が違う点と、迷ったときの3つの判断条件を整理しました。
◈ Rork 入門2026-09-14
Rork の新規作成から Expo が消えました。手元のプロジェクトを続けるか、SwiftUI へ作り直すか
Rork では Expo(React Native)の新規プロジェクトを作れなくなりました。既存の Expo アプリは動き続けます。続けるか SwiftUI へ作り直すかを、公式ドキュメントの記述と、作り直しで実際に抜け落ちる場所から判断する材料をまとめます。
◈ Rork 入門2026-09-08
Rork Max が要るかどうかは、料金表ではなく権限の棚卸しで判断できます
Rork Max に上げるべきか迷ったとき、料金表を先に見ないようにしています。app.json と Info.plist から実際に要求している OS 機能を抜き出し、標準版で届くもの・設定で届くもの・ネイティブが要るものへ仕分ける手順を書き残します。