app.json の plugins は空のままで、位置情報の許可ダイアログを出すコードも、まだ書いていない段階でした。そこで念のため、iOS のプロジェクトを書き出して Info.plist を開いてみました。
そこにあったのは、カメラ、連絡先、マイク、モーション、写真ライブラリの説明文でした。どれも「Allow $(PRODUCT_NAME) to access your camera」のような英語の既定文のままです。
AI にアプリを作ってもらうと、機能を足すたびにパッケージが増えていきます。そのたびに、使い道のない権限の説明文もこっそり増えているのではないか。その数を、実際に数えてみました。
測ったこと
検証用の空のプロジェクトを作り、次のパッケージを入れました。Expo SDK 58 の版で、npx expo install が選んだものです。
| パッケージ | 版 |
|---|---|
| expo | 58.0.14 |
| react-native | 0.88.0 |
| expo-location | 58.0.17 |
| expo-camera | 58.0.10 |
| expo-contacts | 58.0.7 |
| expo-media-library | 58.0.10 |
| expo-image-picker | 58.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 件、すべて英語の既定文でした
| キー | 由来(外したときに消えた設定) |
|---|---|
| NSCameraUsageDescription | expo-camera |
| NSContactsUsageDescription | expo-contacts(contactsPermission: false で消えました) |
| NSLocationWhenInUseUsageDescription | expo-location |
| NSLocationAlwaysUsageDescription | expo-location |
| NSLocationAlwaysAndWhenInUseUsageDescription | expo-location |
| NSMotionUsageDescription | expo-location(プラグインのソースに記述があります) |
| NSMicrophoneUsageDescription | expo-camera と expo-image-picker(両方で microphonePermission: false にすると消えました) |
| NSPhotoLibraryUsageDescription | expo-image-picker と expo-media-library |
| NSPhotoLibraryAddUsageDescription | expo-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 では、権限の説明文が無いまま、該当の機能(カメラ、位置情報など)を呼び出すと、アプリが終了します。「使っていない」と思っていた機能が、実は別の画面から呼ばれているかもしれません。
私は次の順で進めます。
- コードの中で、カメラ・連絡先・位置情報などを呼んでいる箇所を検索します。
- 呼んでいない権限を
used-permissions.jsonに入れないまま、スクリプトを走らせます。 NGの鍵について、パッケージごと外すか、falseで止めるかを決めます。- 残す鍵の説明文を、その画面で何に使うのかが分かる自分の言葉に書き直します。
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 を開いてみていただければ幸いです。数は、思っていたより多いかもしれません。