React Nativeアプリでカメラや位置情報へアクセスするには、OSレベルのパーミッション(権限)取得が必須です。iOS/Androidで仕組みが異なるうえ、OSバージョンごとの仕様変更も頻繁に発生するため、パーミッション管理はモバイル開発で最もつまずきやすい領域の1つです。
react-native-permissionsは、iOS・Android・Windowsのパーミッション操作を統一APIで扱えるライブラリです。GitHub Star数は4,300超、npm週間ダウンロード数は約50万件に達しており、React Nativeにおけるパーミッション管理のデファクトスタンダードとなっています。
react-native-permissionsの基本概念
ライブラリの役割と位置づけ
React Native本体にもAndroid向けのPermissionsAndroid APIが用意されていますが、iOS向けの公式パーミッションAPIは存在しません。react-native-permissionsはこのギャップを埋め、クロスプラットフォームで一貫した権限管理を実現します。
| 項目 | 値 |
|---|---|
| 最新安定版 | 5.5.0(2026年2月リリース) |
| 対応プラットフォーム | iOS 13.4以上 / Android 6.0以上 / Windows |
| 対応React Native | 0.76.0以上(v5.0.0時点では0.73.0以上) |
| ライセンス | MIT |
パーミッションステータスの5状態
react-native-permissionsでは、すべてのパーミッションが以下の5つのステータスで表現されます。
| ステータス | 意味 | 具体例 |
|---|---|---|
UNAVAILABLE | デバイスがその機能に非対応 | カメラ非搭載のデバイスでカメラ権限を確認 |
DENIED | 未リクエスト、またはリクエスト可能な拒否状態 | 初回起動時やユーザーが「許可しない」を選択(再リクエスト可能) |
GRANTED | ユーザーが許可済み | 正常に機能を利用できる状態 |
LIMITED | 制限付きの許可 | iOS 14以降のフォトライブラリ「選択した写真」やiOS 18以降の連絡先 |
BLOCKED | 永続的に拒否され、システム設定からのみ変更可能 | Androidで「今後表示しない」にチェック後の拒否、iOSで一度拒否した状態 |
Androidの重要な仕様: check()やcheckMultiple()はBLOCKEDを返しません。BLOCKEDかどうかを判定するにはrequest()の呼び出しが必要です。
インストールとプラットフォーム別セットアップ
パッケージのインストール
npm install react-native-permissions
# または
yarn add react-native-permissions
iOS側の設定
iOSでは、使用するパーミッションごとにネイティブハンドラをPodfileで選択的に組み込む仕組みになっています。未使用のパーミッション関連コードがバイナリに含まれるとApp Storeの審査でリジェクトされるため、この設定は非常に重要です。
Podfileの編集:
# Podfile先頭にヘルパー関数を追加
def node_require(script)
require Pod::Executable.execute_command('node', ['-p',
"require.resolve('#{script}', {paths: [process.argv[1]]})",
__dir__]).strip
end
node_require('react-native/scripts/react_native_pods.rb')
node_require('react-native-permissions/scripts/setup.rb')
# 使用するパーミッションだけを指定(不要なものは削除)
setup_permissions([
'Camera',
'LocationWhenInUse',
'Microphone',
'Notifications',
'PhotoLibrary',
])
setup.rbは指定されたパーミッションのみをRNPermissions.podspecに組み込むスクリプトです。必要なものだけを列挙してください。
Pod Install の実行:
cd ios && pod install
Info.plistへの使用理由の追加:
各パーミッションに対応するNS*UsageDescriptionキーをInfo.plistへ追記します。
<key>NSCameraUsageDescription</key>
<string>プロフィール写真の撮影とQRコード読み取りに使用します</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>近くの店舗検索と配達時間の算出に使用します</string>
<key>NSMicrophoneUsageDescription</key>
<string>音声メッセージの録音とビデオ通話に使用します</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>投稿やメッセージに添付する画像の選択に使用します</string>
Appleの審査では「なぜそのデータが必要か」を具体的に記述することが求められます。「カメラを使用するため」のような曖昧な文言はリジェクト対象となるため、必ず具体的なユースケースを含めてください。
Android側の設定
AndroidではAndroidManifest.xmlに使用するパーミッションを宣言するだけで完了します。iOSのようなPodfile設定は不要です。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Android 13以降のメディアアクセス -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Android 14以降の選択的アクセス -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
</manifest>
Expo環境での設定
Expo(Managed Workflow)を使用している場合は、Config Pluginで設定できます。
app.json での設定例:
{
"expo": {
"plugins": [
[
"react-native-permissions",
{
"iosPermissions": ["Camera", "Microphone", "PhotoLibrary"]
}
]
],
"ios": {
"infoPlist": {
"NSCameraUsageDescription": "写真撮影に使用します",
"NSMicrophoneUsageDescription": "音声録音に使用します",
"NSPhotoLibraryUsageDescription": "画像選択に使用します"
}
},
"android": {
"permissions": [
"android.permission.CAMERA",
"android.permission.RECORD_AUDIO"
]
}
}
}
設定後にnpx expo prebuildを実行すると、ネイティブファイルが自動生成されます。
主要APIリファレンス
check — 現在のステータス確認
import { check, PERMISSIONS, RESULTS } from 'react-native-permissions';
import { Platform } from 'react-native';
const permission = Platform.select({
ios: PERMISSIONS.IOS.CAMERA,
android: PERMISSIONS.ANDROID.CAMERA,
})!;
const status = await check(permission);
// => 'unavailable' | 'denied' | 'granted' | 'limited' | 'blocked'
check()はダイアログを表示せず、現在の状態を取得するだけです。初回レンダリング時やUIの切り替え判定に使用します。
request — パーミッションのリクエスト
import { request, PERMISSIONS, RESULTS } from 'react-native-permissions';
const status = await request(
Platform.OS === 'ios'
? PERMISSIONS.IOS.CAMERA
: PERMISSIONS.ANDROID.CAMERA
);
if (status === RESULTS.GRANTED) {
// カメラ機能を有効化
}
request()はOSのパーミッションダイアログを表示し、ユーザーの応答結果を返します。Androidではrationale引数を渡すことで、システムダイアログの前に説明アラートを表示できます。
// Android向けの説明ダイアログ付きリクエスト
const status = await request(
PERMISSIONS.ANDROID.CAMERA,
{
title: 'カメラへのアクセス',
message: 'プロフィール写真の撮影にカメラを使用します',
buttonPositive: '許可する',
buttonNegative: 'キャンセル',
}
);
checkMultiple / requestMultiple — 複数パーミッションの一括処理
import { requestMultiple, PERMISSIONS } from 'react-native-permissions';
const statuses = await requestMultiple([
PERMISSIONS.ANDROID.CAMERA,
PERMISSIONS.ANDROID.RECORD_AUDIO,
PERMISSIONS.ANDROID.READ_MEDIA_IMAGES,
]);
console.log(statuses[PERMISSIONS.ANDROID.CAMERA]); // => 'granted'
console.log(statuses[PERMISSIONS.ANDROID.RECORD_AUDIO]); // => 'denied'
ビデオ通話やメディア投稿など、複数権限が同時に必要な機能で使用します。
checkNotifications / requestNotifications — 通知パーミッション
通知は他のパーミッションと異なる専用APIで管理します。
import { checkNotifications, requestNotifications } from 'react-native-permissions';
// ステータスと詳細設定を取得
const { status, settings } = await checkNotifications();
// iOSではアラート・バッジ・サウンドの個別指定が可能
const result = await requestNotifications(['alert', 'badge', 'sound']);
openSettings — 設定画面への誘導
ユーザーが権限をブロックした場合、アプリ側からは再リクエストできません。openSettings()でOS設定画面を開き、手動で有効化してもらう必要があります。
import { openSettings } from 'react-native-permissions';
// アプリ設定画面を開く
await openSettings();
// 特定の設定画面を開く(通知設定など)
await openSettings('notifications');
iOS / Androidのパーミッションフロー比較
iOS/Androidではパーミッションの状態遷移が大きく異なります。この違いを理解しておかないと、プラットフォームごとに異なる不具合が発生します。
iOSのフロー
アプリ起動
└─ check() で確認
├─ UNAVAILABLE → 機能非対応(ハードウェア不在など)
├─ DENIED → 未リクエスト状態。request() 可能
│ └─ request() 実行
│ ├─ GRANTED → 許可された
│ ├─ LIMITED → 制限付き許可(写真の部分アクセスなど)
│ └─ BLOCKED → 拒否された(以降はシステム設定からのみ変更可能)
├─ GRANTED → 許可済み
├─ LIMITED → 制限付き許可
└─ BLOCKED → 設定画面で変更が必要
iOSではシステムダイアログはパーミッションの種類ごとに1回しか表示されません。ユーザーが拒否すると、以降はopenSettings()で設定画面へ誘導するしか方法がありません。
Androidのフロー
アプリ起動
└─ check() で確認
├─ UNAVAILABLE → 機能非対応
├─ DENIED → 未リクエストまたは再リクエスト可能
│ └─ request() 実行
│ ├─ GRANTED → 許可された
│ ├─ DENIED → 拒否された(再リクエスト可能)
│ └─ BLOCKED → 「今後表示しない」で拒否された
└─ GRANTED → 許可済み
Androidでは拒否されても再リクエストが可能ですが、ユーザーが「今後表示しない」をチェックして拒否するとBLOCKEDになります。また、check()はBLOCKEDを返さないため、BLOCKEDかどうかはrequest()の結果で判定する点に注意が必要です。
プラットフォーム差異の早見表
| 挙動 | iOS | Android |
|---|---|---|
| ダイアログ表示回数 | 1回のみ | 複数回可能(「今後表示しない」選択まで) |
check()でBLOCKEDを返すか | 返す | 返さない |
LIMITEDステータス | あり(写真・連絡先) | あり(Android 14以降の写真選択) |
rationale引数 | 無視される | システムダイアログ前に説明アラートを表示 |
| ワンタイムパーミッション | なし | Android 11以降で対応 |
実践的なカスタムHookの設計
パーミッション処理をコンポーネントから分離し、再利用可能なHookとして設計すると保守性が大きく向上します。
汎用パーミッションHook
import { useState, useEffect, useCallback } from 'react';
import {
check,
request,
openSettings,
Permission,
PermissionStatus,
RESULTS,
} from 'react-native-permissions';
import { AppState, AppStateStatus } from 'react-native';
interface UsePermissionReturn {
status: PermissionStatus | null;
isGranted: boolean;
isBlocked: boolean;
requestPermission: () => Promise<PermissionStatus>;
goToSettings: () => Promise<void>;
}
export function usePermission(permission: Permission): UsePermissionReturn {
const [status, setStatus] = useState<PermissionStatus | null>(null);
const checkPermission = useCallback(async () => {
const result = await check(permission);
setStatus(result);
return result;
}, [permission]);
const requestPermission = useCallback(async () => {
const result = await request(permission);
setStatus(result);
return result;
}, [permission]);
const goToSettings = useCallback(async () => {
await openSettings();
}, []);
// 初回マウント時にステータスを確認
useEffect(() => {
checkPermission();
}, [checkPermission]);
// 設定画面から戻ったときにステータスを再確認
useEffect(() => {
let currentState = AppState.currentState;
const subscription = AppState.addEventListener(
'change',
(nextState: AppStateStatus) => {
if (
currentState.match(/inactive|background/) &&
nextState === 'active'
) {
checkPermission();
}
currentState = nextState;
},
);
return () => subscription.remove();
}, [checkPermission]);
return {
status,
isGranted: status === RESULTS.GRANTED || status === RESULTS.LIMITED,
isBlocked: status === RESULTS.BLOCKED,
requestPermission,
goToSettings,
};
}
このHookのポイントはAppStateのリスナーです。ユーザーがopenSettings()で設定画面に遷移し、権限を変更してアプリに戻った際に、ステータスを自動的に再取得します。
カメラ機能での使用例
import React from 'react';
import { View, Text, Button, Alert, Platform } from 'react-native';
import { PERMISSIONS, RESULTS } from 'react-native-permissions';
import { usePermission } from './hooks/usePermission';
const cameraPermission = Platform.select({
ios: PERMISSIONS.IOS.CAMERA,
android: PERMISSIONS.ANDROID.CAMERA,
})!;
export function CameraScreen() {
const { status, isGranted, isBlocked, requestPermission, goToSettings } =
usePermission(cameraPermission);
const handlePress = async () => {
if (isGranted) {
// カメラを起動
return;
}
const result = await requestPermission();
if (result === RESULTS.BLOCKED) {
Alert.alert(
'カメラへのアクセスが必要です',
'設定画面からカメラへのアクセスを許可してください。',
[
{ text: 'キャンセル', style: 'cancel' },
{ text: '設定を開く', onPress: goToSettings },
],
);
}
};
if (status === null) {
return null; // ステータス確認中
}
return (
<View>
{isGranted ? (
<Text>カメラが利用可能です</Text>
) : (
<Button title="カメラを使用する" onPress={handlePress} />
)}
</View>
);
}
複数パーミッションを一括管理するHook
ビデオ通話のようにカメラ・マイクを同時に必要とする場合は、複数パーミッションをまとめて管理するHookが便利です。
import { useState, useCallback, useEffect } from 'react';
import {
checkMultiple,
requestMultiple,
Permission,
PermissionStatus,
RESULTS,
} from 'react-native-permissions';
import { AppState, AppStateStatus } from 'react-native';
export function useMultiplePermissions(permissions: Permission[]) {
const [statuses, setStatuses] = useState<Record<string, PermissionStatus>>({});
const checkAll = useCallback(async () => {
const results = await checkMultiple(permissions);
setStatuses(results);
return results;
}, [permissions]);
const requestAll = useCallback(async () => {
const results = await requestMultiple(permissions);
setStatuses(results);
return results;
}, [permissions]);
const allGranted = permissions.every(
(p) => statuses[p] === RESULTS.GRANTED || statuses[p] === RESULTS.LIMITED,
);
const blockedPermissions = permissions.filter(
(p) => statuses[p] === RESULTS.BLOCKED,
);
useEffect(() => {
checkAll();
}, [checkAll]);
useEffect(() => {
let currentState = AppState.currentState;
const subscription = AppState.addEventListener(
'change',
(nextState: AppStateStatus) => {
if (currentState.match(/inactive|background/) && nextState === 'active') {
checkAll();
}
currentState = nextState;
},
);
return () => subscription.remove();
}, [checkAll]);
return { statuses, checkAll, requestAll, allGranted, blockedPermissions };
}
Android 13以降の細分化メディアパーミッション
Android 13(API 33)で従来のREAD_EXTERNAL_STORAGEが廃止され、メディア種別ごとのパーミッションに分割されました。さらにAndroid 14(API 34)では「選択した写真のみ」のアクセスが追加されています。
バージョン別の対応パーミッション
| Androidバージョン | パーミッション | 動作 |
|---|---|---|
| 12以前(API 32以下) | READ_EXTERNAL_STORAGE | 全メディアへのフルアクセス |
| 13(API 33) | READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / READ_MEDIA_AUDIO | メディア種別ごとのアクセス |
| 14以降(API 34以上) | READ_MEDIA_VISUAL_USER_SELECTED | ユーザーが選択した写真・動画のみ |
Androidバージョン別のパーミッション処理
import { Platform } from 'react-native';
import { requestMultiple, PERMISSIONS, RESULTS } from 'react-native-permissions';
async function requestMediaPermissions() {
if (Platform.OS !== 'android') {
// iOS の場合
return request(PERMISSIONS.IOS.PHOTO_LIBRARY);
}
const apiLevel = Platform.Version;
if (apiLevel >= 34) {
// Android 14以降:選択的アクセス対応
const statuses = await requestMultiple([
PERMISSIONS.ANDROID.READ_MEDIA_IMAGES,
PERMISSIONS.ANDROID.READ_MEDIA_VIDEO,
PERMISSIONS.ANDROID.READ_MEDIA_VISUAL_USER_SELECTED,
]);
return statuses;
}
if (apiLevel >= 33) {
// Android 13:メディア種別ごとのパーミッション
const statuses = await requestMultiple([
PERMISSIONS.ANDROID.READ_MEDIA_IMAGES,
PERMISSIONS.ANDROID.READ_MEDIA_VIDEO,
]);
return statuses;
}
// Android 12以前
return request(PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE);
}
Android 14でREAD_MEDIA_VISUAL_USER_SELECTEDを導入しないと、互換モードで動作します。互換モードではREAD_MEDIA_IMAGES/READ_MEDIA_VIDEOがワンタイムパーミッション(セッション限定の一時的許可)として扱われ、アプリを再起動するたびにユーザーが再許可する必要が生じます。
対応パーミッション一覧
iOS(20種類)
| 定数 | Info.plistキー | 用途 |
|---|---|---|
APP_TRACKING_TRANSPARENCY | NSUserTrackingUsageDescription | 広告トラッキング(ATT) |
BLUETOOTH | NSBluetoothAlwaysUsageDescription | Bluetooth機器接続 |
CALENDARS | NSCalendarsUsageDescription | カレンダー読み書き |
CALENDARS_WRITE_ONLY | NSCalendarsWriteOnlyAccessUsageDescription | カレンダー書き込みのみ |
CAMERA | NSCameraUsageDescription | カメラ撮影 |
CONTACTS | NSContactsUsageDescription | 連絡先アクセス |
FACE_ID | NSFaceIDUsageDescription | Face ID認証 |
LOCATION_WHEN_IN_USE | NSLocationWhenInUseUsageDescription | フォアグラウンド位置情報 |
LOCATION_ALWAYS | NSLocationAlwaysAndWhenInUseUsageDescription | バックグラウンド位置情報 |
MEDIA_LIBRARY | NSAppleMusicUsageDescription | Apple Music/メディアライブラリ |
MICROPHONE | NSMicrophoneUsageDescription | マイク録音 |
MOTION | NSMotionUsageDescription | モーション・フィットネスデータ |
PHOTO_LIBRARY | NSPhotoLibraryUsageDescription | フォトライブラリ読み取り |
PHOTO_LIBRARY_ADD_ONLY | NSPhotoLibraryAddUsageDescription | フォトライブラリ追加のみ |
REMINDERS | NSRemindersUsageDescription | リマインダーアクセス |
SIRI | NSSiriUsageDescription | Siri連携 |
SPEECH_RECOGNITION | NSSpeechRecognitionUsageDescription | 音声認識 |
Android(主要なもの)
| 定数 | AndroidManifestの宣言 | 用途 |
|---|---|---|
CAMERA | android.permission.CAMERA | カメラ撮影 |
RECORD_AUDIO | android.permission.RECORD_AUDIO | マイク録音 |
ACCESS_FINE_LOCATION | android.permission.ACCESS_FINE_LOCATION | 精密位置情報 |
ACCESS_COARSE_LOCATION | android.permission.ACCESS_COARSE_LOCATION | おおまかな位置情報 |
ACCESS_BACKGROUND_LOCATION | android.permission.ACCESS_BACKGROUND_LOCATION | バックグラウンド位置情報 |
READ_MEDIA_IMAGES | android.permission.READ_MEDIA_IMAGES | 画像読み取り(API 33以降) |
READ_MEDIA_VIDEO | android.permission.READ_MEDIA_VIDEO | 動画読み取り(API 33以降) |
READ_MEDIA_AUDIO | android.permission.READ_MEDIA_AUDIO | 音声読み取り(API 33以降) |
READ_MEDIA_VISUAL_USER_SELECTED | 同名 | 選択的メディアアクセス(API 34以降) |
POST_NOTIFICATIONS | android.permission.POST_NOTIFICATIONS | 通知表示(API 33以降) |
BLUETOOTH_CONNECT | android.permission.BLUETOOTH_CONNECT | Bluetooth接続 |
BLUETOOTH_SCAN | android.permission.BLUETOOTH_SCAN | Bluetoothスキャン |
READ_CONTACTS | android.permission.READ_CONTACTS | 連絡先読み取り |
READ_CALENDAR | android.permission.READ_CALENDAR | カレンダー読み取り |
Androidでは全44種類のパーミッション定数が用意されています。完全な一覧は公式リポジトリのREADMEで確認できます。
パーミッションUXのベストプラクティス
パーミッション取得に成功するかどうかは、技術実装だけでなくUX設計に大きく依存します。米国のUX調査機関Nielsen Norman Groupの研究によると、パーミッションリクエストのタイミングと説明の質が許可率を大幅に左右します。
原則1:機能の文脈でリクエストする
アプリ起動直後にすべてのパーミッションをまとめて要求するのは避けてください。ユーザーがその機能を実際に使おうとしたタイミングでリクエストすると、許可率が大幅に向上します。
- カメラアイコンをタップしたとき → カメラパーミッションを要求
- 「現在地から探す」ボタンを押したとき → 位置情報パーミッションを要求
- 音声メッセージ録音を開始するとき → マイクパーミッションを要求
例外として、ナビゲーションアプリの位置情報のようにアプリのコア機能に不可欠なパーミッションは、オンボーディング時にリクエストしても問題ありません。
原則2:事前説明画面(プリパーミッション)を挟む
iOSではシステムダイアログがパーミッションの種類ごとに1回しか表示されないため、ユーザーがそこで拒否するとアプリ側から再リクエストできません。この制約に対処する手法が「ダブルダイアログパターン」です。
[1] アプリ独自の説明画面(理由と利点を提示)
├─ 「許可する」 → [2] OSのシステムダイアログを表示
└─ 「あとで」 → システムダイアログは表示しない(権限を温存)
説明画面では「なぜ必要か」「ユーザーにとってどんな利点があるか」を1〜2文で伝えます。Tan et al.の研究(出典: Nielsen Norman Groupが引用)によると、最も説得力のある説明文と最も弱い説明文の間で許可率に81%の差が生じたと報告されています。説明文の内容がユーザーの判断に直結するため、具体的な利点を記載することが重要です。
原則3:拒否後もアプリを使える状態を維持する
パーミッションが拒否されてもアプリ全体がフリーズしたり、エラー画面で止まったりしてはなりません。該当機能だけを無効化し、他の機能は通常通り利用できる状態を維持してください。
// パーミッションがBLOCKEDの場合のフォールバック表示例
function PhotoPicker({ permission }: { permission: PermissionStatus }) {
if (permission === RESULTS.BLOCKED) {
return (
<View style={styles.fallback}>
<Text>写真へのアクセスが許可されていません</Text>
<Button
title="設定画面を開く"
onPress={() => openSettings()}
/>
</View>
);
}
// 通常のフォトピッカーUI
return <PhotoGrid />;
}
原則4:しつこく再リクエストしない
一度拒否されたパーミッションを、画面遷移のたびに何度もリクエストするのはユーザー体験を著しく損ないます。再リクエストは、ユーザーが該当機能を明示的に操作したときだけに限定してください。
Jestでのパーミッションテスト
react-native-permissionsはビルトインのモックモジュールを提供しており、ユニットテストが容易に書けます。
モックのセットアップ
jest.setup.js:
jest.mock('react-native-permissions', () =>
require('react-native-permissions/mock')
);
jest.config.js:
module.exports = {
setupFiles: ['<rootDir>/jest.setup.js'],
};
ステータス別のテスト記述例
import { check, request, RESULTS } from 'react-native-permissions';
import { renderHook, act } from '@testing-library/react-hooks';
import { usePermission } from './usePermission';
describe('usePermission', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('初期状態でGRANTEDならisGrantedがtrueになる', async () => {
(check as jest.Mock).mockResolvedValue(RESULTS.GRANTED);
const { result, waitForNextUpdate } = renderHook(() =>
usePermission('ios.permission.CAMERA')
);
await waitForNextUpdate();
expect(result.current.isGranted).toBe(true);
});
it('DENIEDからrequestでGRANTEDに遷移する', async () => {
(check as jest.Mock).mockResolvedValue(RESULTS.DENIED);
(request as jest.Mock).mockResolvedValue(RESULTS.GRANTED);
const { result, waitForNextUpdate } = renderHook(() =>
usePermission('ios.permission.CAMERA')
);
await waitForNextUpdate();
expect(result.current.isGranted).toBe(false);
await act(async () => {
await result.current.requestPermission();
});
expect(result.current.isGranted).toBe(true);
});
it('BLOCKEDの場合isBlockedがtrueになる', async () => {
(check as jest.Mock).mockResolvedValue(RESULTS.BLOCKED);
const { result, waitForNextUpdate } = renderHook(() =>
usePermission('ios.permission.CAMERA')
);
await waitForNextUpdate();
expect(result.current.isBlocked).toBe(true);
});
});
E2Eテスト(Detox)でのパーミッション操作
Detoxを使ったE2Eテストでは、iOSはアプリ起動時の設定、AndroidはADBコマンドでパーミッション状態を制御します。
// iOS: アプリ起動時にパーミッションを事前設定
await device.launchApp({
permissions: { camera: 'YES', location: 'always' },
});
// Android: ADBでパーミッションを付与/取り消し
execSync(
`adb shell pm grant com.example.app android.permission.CAMERA`
);
execSync(
`adb shell pm revoke com.example.app android.permission.CAMERA`
);
App Store / Google Playの審査で押さえるべきポイント
iOS: Guideline 5.1.1(Privacy)対策
App Store審査で最も頻出するリジェクト理由の1つがGuideline 5.1.1です。対策として以下を確認してください。
- すべてのパーミッションにInfo.plistの使用理由を記載する: 具体的なユースケースを含めた1〜2文の説明を書く
- 対応言語すべてにローカライズする: Usage Descriptionは端末の言語設定で表示されるため、多言語対応が必要
- 未使用のパーミッション関連コードを含めない: Podfileの
setup_permissionsで必要なものだけを指定する - サードパーティSDKのパーミッション利用を把握する: Firebase、広告SDK等が暗黙的に要求するパーミッションもInfo.plistへの記載が必要
- App Privacyの質問票を正確に記入する: App Store Connectの「Appのプライバシー」セクションで、収集するデータの種類と用途を正しく申告する
Android: Google Playポリシー対策
- 不要なパーミッションをManifestから削除する: 使わないパーミッションが宣言されていると審査で指摘される可能性がある
READ_EXTERNAL_STORAGEをAPI 33以降で使用しない:READ_MEDIA_*系に移行する- バックグラウンド位置情報の正当性を示す:
ACCESS_BACKGROUND_LOCATIONを使う場合、Google Playの審査フォームで具体的な利用理由を提出する必要がある
よくあるトラブルと解決策
1. iOSでcheck()がDENIEDを返し続ける
Podfileで対象パーミッションのsetup_permissionsへの追加を忘れているケースが最も多い原因です。pod install後にXcodeのBuild PhasesでPermissionハンドラが組み込まれているか確認してください。
2. AndroidのcheckでBLOCKEDが検出できない
仕様上、Androidのcheck()とcheckMultiple()はBLOCKEDを返しません。BLOCKEDかどうかを判定するにはrequest()を呼ぶ必要があります。
// Androidでの正しいBLOCKED判定パターン
const status = await request(PERMISSIONS.ANDROID.CAMERA);
if (status === RESULTS.BLOCKED) {
// 設定画面へ誘導
}
3. 設定画面から戻ってもUIが更新されない
openSettings()で設定画面に遷移後、アプリに戻ったときにパーミッション状態を再チェックしていないことが原因です。前述のカスタムHookのようにAppStateの変化を監視し、activeに戻ったタイミングでcheck()を再実行してください。
4. Android 13でストレージパーミッションが取得できない
Android 13以降ではREAD_EXTERNAL_STORAGEが非推奨です。メディアの種類に応じたREAD_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIOに移行してください。AndroidManifest.xmlでの宣言も変更する必要があります。
5. Expo prebuild後にパーミッションが動作しない
Config Pluginの設定後はnpx expo prebuild --cleanで一度クリーンビルドを実行してください。キャッシュされた古いネイティブコードが原因で設定が反映されないことがあります。
v5系のアップグレード時の注意点
react-native-permissions v4以前からv5系にアップグレードする際の主な変更点です。
| 変更内容 | 詳細 |
|---|---|
| 最低動作要件の引き上げ | Xcode 16、iOS 13.4以上、Android 6.0以上、React Native 0.73以上 |
| AndroidネイティブコードのKotlin移行 | Java → Kotlinに移行。android/build.gradleの設定確認が必要 |
requestNotificationsのrationale引数追加 | Android向けに説明ダイアログ表示のオプションが追加 |
iOS 18の連絡先LIMITED対応 | PERMISSIONS.IOS.CONTACTSがLIMITEDを返す可能性がある |
| 位置情報のエスカレーション | iOSでLOCATION_WHEN_IN_USE → LOCATION_ALWAYSの段階的リクエストに対応 |
まとめ
react-native-permissionsを使うことで、iOS/Androidの複雑なパーミッション仕様を統一APIで管理できます。
実装時に特に意識すべきポイントは以下の3つです。
- プラットフォーム差異の把握: iOSはダイアログが1回限り、Androidは
check()でBLOCKEDを検出できないなど、同じAPIでも動作が異なる - ユーザー体験の設計: 機能の文脈でリクエストする、事前説明画面を挟む、拒否後のフォールバックUIを用意する
- OSバージョン対応: Android 13以降のメディアパーミッション細分化やiOS 14以降の
LIMITEDステータスなど、継続的なキャッチアップが求められる
パーミッション管理はアプリの信頼性とストア審査の合否に直結する重要な領域です。カスタムHookによる設計パターンやJestモックによるテストを活用し、堅牢な権限管理を実現してください。
