まず確認すること: Info.plist のシステムコード登録が原因かどうか
iOS 17 で FeliCa が読めなくなった場合、最初に疑うべきは Info.plist に登録したシステムコード数が iOS 17 の処理上限に達していることです。上限に起因する問題は症状が「読めたり読めなかったりする」という断続的な不安定さとして現れるため、ハードウェア不良やNFCセッション管理のバグと混同されやすい点に注意が必要です。
Core NFC で FeliCa を読み取る仕組みの概要
Core NFC とは、Apple が iOS 13 から FeliCa 読み取りに対応したフレームワークです(出典: Apple Developer Documentation「Core NFC」、および WWDC 2020 セッション 10209「Core NFC Enhancements」)。
FeliCa 読み取りの流れは次のとおりです。
- 設定の準備: エンタイトルメントファイル(
.entitlements)と Info.plist の2箇所にcom.apple.developer.nfc.readersession.felica.systemcodesキーを記載し、読み取り対象のシステムコードを文字列配列として列挙する。どちらか一方が欠けると「Missing required entitlement」エラーが発生します(出典: Apple Developer Forums スレッド #685809「NFC (entitlements)」、スレッド #781710「NFCTagReaderSession - Missing required entitlement」)。 - セッション開始:
NFCTagReaderSessionを初期化し、デリゲートでNFCFeliCaTagプロトコルのメソッドを実装する。 - ポーリングと検出: セッション開始後、端末が NFC フィールドを有効化し、登録済みシステムコードを持つタグを検出する。
- データ取得: タグ検出後、
currentSystemCodeやcurrentIDmを取得してアプリのロジックへ渡す。
ここで重要な制約があります。Apple Developer Forums スレッド #122504 および GitHub metrodroid/metrodroid Issue #613(https://github.com/metrodroid/metrodroid/issues/613)で報告されているとおり、**1回の NFCTagReaderSession でポーリング対象にできるシステムコードは1件のみ**です。複数のシステムコードを持つ FeliCa カードを正しく読み取れないケースはこの制約に起因しており、バージョンをまたいで再現が確認されています。
対応デバイスは iPhone 7 以降の全モデルです。ただし FeliCa 読み取りを実機で試験する際は、日本国内で入手したデバイスを使用することを推奨します。
症状から原因を切り分けるチェックリスト
下表で、症状ごとに「システムコード登録数の上限が原因か否か」を一次判定できます。チェックがすべて「関係薄」であれば、タイムアウトやセッション管理など別の原因を疑い、詳しい診断は後続セクションで扱います。
| 症状 | システムコード登録数が原因の可能性 | 次のアクション |
|---|---|---|
| 特定カード種別だけ読めない、他は読める | 高い | 該当カードのシステムコードが Info.plist に含まれているか確認 |
| iOS バージョンアップ後に突然読めなくなった | 高い | バージョンごとの処理上限を後述の比較表で確認 |
| 登録コード数が多いほど失敗率が上がる | 高い | 登録数を段階的に削減して再検証 |
| 読み取り成功率は変わらないが遅くなった | 低い | 読み取り速度の最適化手順を参照してください |
| 特定端末モデルでのみ発生する | 低い | 端末固有の NFC チップ挙動を疑う |
| エラーログに「Missing required entitlement」が出る | 原因が異なる | エンタイトルメントと Info.plist の両方に登録されているか確認 |
以上のチェックリストで「高い」に該当する症状がある場合、次のセクションで iOS 17 の具体的な上限値と挙動を確認してください。
iOS 17 におけるシステムコード登録数の上限とは?
当社の実機調査では、iOS 17 では Info.plist に登録できるシステムコードの処理上限が約 148 個であることを特定しています。ただしこの上限以前に、より根本的な動作制約として押さえておくべき仕様があります。
上限を超えたときに起きる挙動
Apple Developer Forums(スレッド #122504「CoreNFC/FeliCa bug: cannot read da…」)および GitHub の metrodroid/metrodroid Issue #613「iOS: FeliCa with more than 1 system code cannot be read」では、1 回の NFCTagReaderSession でポーリング対象にできるシステムコードは 1 件のみ という動作制約が報告されています。
つまり、Info.plist に複数のシステムコードを列挙していても、1 セッションで実際に検索対象となるのは 1 件だけです。この制約を踏まえた上で、登録数が iOS 17 の処理上限(当社実機調査: 約 148 個)を超えた場合の挙動は以下のとおりです。
| 登録数の状態 | 発生する挙動 |
|---|---|
| 1 件 | 正常に検出・読み取りできる |
| 2 件以上・上限以内 | セッションごとに 1 件しかポーリングされないため、対象外コードのカードは読み取れない |
| 約 148 件超(iOS 17 実測) | カード自体が検出されない、またはセッション開始直後にエラーで終了する |
上限超過時に表示されるエラーコードは NFCReaderSessionInvalidationErrorSystemIsBusy や NFCReaderErrorUnsupportedFeature として現れるケースが多く、ログだけでは登録数が原因とは気づきにくいため注意が必要です。
なお、2 件以上のシステムコードを持つ物理カード(例: 交通系 IC と電子マネー機能が共存するカード)の読み取り失敗も、同一の根本原因から生じます。この問題への対処は「Info.plist への正しいシステムコード設定手順」セクションで詳しく扱います。
iOS 16 / iOS 18 / iOS 26 との上限比較
当社の実機調査で計測したバージョン別の処理上限を以下にまとめます。数値はすべて同一テスト環境(機種・試行回数を統一)での実測値です。バージョン間の検出時間の詳細な比較については、別記事「iOS NFC FeliCa 検出時間 iOS 17 iOS 18 比較 実測」を参照してください。
| iOS バージョン | システムコード処理上限(実測) | 備考 |
|---|---|---|
| iOS 16 | 当社未計測 | 案件により変動する可能性あり |
| iOS 17 | 約 148 個 | 本記事の対象バージョン |
| iOS 18 | 当社調査中 | 案件により変動する可能性あり |
| iOS 26 | 当社調査中 | 2025年時点でベータ段階 |
Apple は Core NFC の仕様をメジャーアップデートだけでなくマイナアップデートでも変更することがあります。iOS 16 以前や iOS 18 以降の上限値については現時点で確定数値を公表できないため、自社環境での実機計測を推奨します。
また、Core NFC による FeliCa 読み取り自体は iOS 13 から対応しています(出典: Apple Developer Documentation「Core NFC」および WWDC 2020 セッション 10209「Core NFC Enhancements」)。iOS 12 以前のデバイスはそもそも FeliCa 読み取り非対応のため、サポート対象バージョンの下限として必ず確認してください。
設定に必要な箇所は エンタイトルメントファイルと Info.plist の 2 箇所です(出典: Apple Developer Forums スレッド #685809「NFC (entitlements)」、スレッド #781710「NFCTagReaderSession - Missing required entitlement」)。どちらか一方でも登録数が上限を超えた場合、もう一方が正常でも読み取りが失敗します。両ファイルを必ずセットで確認してください。具体的な設定手順は次のセクションで説明します。
Info.plist への正しいシステムコード設定手順
com.apple.developer.nfc.readersession.felica.systemcodes キーに、読み取り対象のシステムコードを16進数文字列の配列として登録するのが基本形です。ただし設定箇所はInfo.plistだけではなく、エンタイトルメントファイルとの2段構えになっている点を押さえてください。
必須設定項目と記述フォーマット
FeliCaをCore NFCで読み取るには、以下の2ファイルへの設定が両方そろって初めて機能します。どちらか一方が欠けていると、ビルドは通ってもランタイムでNFCReaderSessionInvalidationErrorが返ります。
| 設定ファイル | キー名 | 値の形式 | 役割 |
|---|---|---|---|
Info.plist |
com.apple.developer.nfc.readersession.felica.systemcodes |
16進数文字列の配列(例: "8008", "88B4") |
セッション起動時にポーリング対象をOSへ宣言する |
*.entitlements |
com.apple.developer.nfc.readersession.felica.systemcodes |
同上 | App Storeレビュー時および実機起動時の権限チェックに使われる |
Info.plist |
NFCReaderUsageDescription |
任意の日本語説明文字列 | システムダイアログに表示するユーザ向け説明文 |
設定手順は番号順に実施してください。
エンタイトルメントファイルの確認(所要時間: 1〜2分)
Xcodeの「Signing & Capabilities」タブから「Near Field Communication Tag Reading」を追加する。追加済みの場合はスキップ可。つまずきやすい点: 複数ターゲットを持つプロジェクトでは、ターゲットごとにエンタイトルメントが分かれているため、誤ったターゲットに追加しても症状が再現します。エンタイトルメントファイルへのシステムコード記入(所要時間: 2〜5分)
.entitlementsをソースエディタで開き、com.apple.developer.nfc.readersession.felica.systemcodesキーの<array>内に<string>XXXX</string>形式で追加する。16進数は大文字・小文字どちらでも動作しますが、チームで表記を統一しておくと照合が容易です。Info.plist への同一リストの転記(所要時間: 2〜5分)
エンタイトルメントに書いたシステムコードと同一のリストをInfo.plistにも記載する。2ファイルの内容が一致していないとOSが権限エラーを返すことがあります。つまずきやすい点: Xcodeのプロパティリストエディタで入力すると先頭ゼロが数値として丸められる場合があるため、ソースエディタで文字列として直接編集することを推奨します。NFCReaderUsageDescriptionの記載確認(所要時間: 1分)
空文字や未設定のままだとApp Storeの審査で差し戻されます。端末のシステムダイアログに表示される文言なので、読み取り目的が伝わる文章を設定してください。実機での起動確認(所要時間: 5〜10分)
シミュレータはCore NFCを未サポートのため、実機が必須です。XcodeのデバッグコンソールでNFCReaderErrorが出ないことを確認してください。
つまずきやすいポイント: ワイルドカードと固定コードの混在
88B4 はFeliCa規格のワイルドカードに相当するシステムコードで、これを登録すると多くのカード種別をまとめて検出できます。ただし、ワイルドカードと固定コードを配列に混在させる場合には注意が必要です。
混在時に確認すべき3点
- 優先順位の保証がない: iOSがポーリング時にリスト内のシステムコードをどの順序で使うかは非公開仕様です。固定コードを先頭に書いても必ずしも優先されません。
- 登録数にカウントされる:
88B4も1個としてカウントされます。ワイルドカードを使えば合計登録数が自動的に減るわけではありません。登録数の上限については前セクションで触れたとおりです。 - カード種別の取り違えリスク: ワイルドカードでヒットしたカードをアプリ側で種別判定するには、レスポンスのシステムコードフィールドを解析するロジックが別途必要になります。このロジックが不足していると、意図しないカードのデータを読み込む誤作動につながります。
固定コードのみで運用が完結する場合は、ワイルドカードを混在させないほうがデバッグが容易です。読み取り対象カードの種別が確定しておらず、広く拾う必要がある場合のみワイルドカードの採用を検討してください。
登録数を上限以内に収める設計パターン
対象カードの種別を事前にサービス要件として絞り込み、読み取る必要のないシステムコードを登録しないことが最も確実な対策です。「とりあえず全コードを登録する」という実装は、iOS 17 の処理上限(約 148 個)をあっという間に超えます。
パターン1: 用途別にアプリを分割する
1 つのアプリが読み取るカード種別を最小限に絞るのが、最も根本的な解決策です。交通系カード専用アプリと社員証専用アプリを別バイナリにすれば、それぞれの Info.plist に登録するシステムコードは大幅に減ります。
| 分割前 | 分割後(交通系) | 分割後(社員証) |
|---|---|---|
| 180 コード(上限超過) | 32 コード | 12 コード |
- メリット: コード管理がシンプルになり、iOS バージョンアップ時の影響範囲も局所化できる
- デメリット: ユーザが複数アプリをインストールする手間が生じる。ユースケースが明確に分離できる場合にのみ採用する
実装者視点の注意点: アプリを分割しても、エンタイトルメントファイルと Info.plist の両方をそれぞれ修正しなければ機能しません。片方だけ更新してエラーが解消しないケースは頻出です。設定の二箇所同時更新については前セクションで詳述しています。
パターン2: ポーリング前にフィルタリングロジックを挟む
アプリを分割できない場合は、ユーザのアクション(カード種別の選択画面など)をトリガに、その場で必要なシステムコードだけを動的に選んでセッションを開始する設計にします。
手順(所要時間の目安: 実装 2〜4 時間):
- ユーザに「読み取るカードの種別を選択」させる UI を用意する
- つまずきやすい点: 選択肢の粒度が粗いとシステムコード数を絞りきれない。「交通系 IC 全般」ではなく「Suica / PASMO / ICOCA」のように個別選択にする
- 選択結果に対応するシステムコードの配列を、アプリ内の定義テーブルから取り出す
- つまずきやすい点: このテーブルは Info.plist の登録内容と完全に一致している必要がある。Info.plist に存在しないコードを渡してもポーリングは成功しない
NFCTagReaderSessionの初期化時に、取り出した配列をpollingOptionに渡してセッションを開始する- セッション終了後にフィルタ状態をリセットし、次回選択に備える
パターン3: ワイルドカード(88B4)で代替できる範囲を確認する
88B4 はFeliCa のワイルドカードシステムコードで、特定のシステムコードを指定せず広範なカードを検出対象にできます。ただし、全カードが 88B4 で代替できるわけではありません。
| 代替可否 | カード例 | 理由 |
|---|---|---|
| 代替可能 | 独自開発の社員証、会員カードなど | システムコードが非公開または汎用設計のカード |
| 代替不可 | 交通系 IC(Suica 等)、マイナンバーカード | 固有のシステムコードを指定しないと上位サービスへのアクセスが拒否される |
Apple 公式ドキュメントおよびデベロッパフォーラムによると、88B4 を使った場合でも読み取れるデータはカードが公開しているサービスに限定されます。「とにかく検出できればよい」のか「特定のサービスコードまで読む必要があるか」でワイルドカードの採用可否が決まります。
パターン選択フロー:
対象カード種別を 1 種類に絞れるか?
├─ Yes → パターン 1(アプリ分割)を検討
└─ No
├─ ユーザが読取前に種別を選択できるか?
│ ├─ Yes → パターン 2(動的フィルタ)を採用
│ └─ No
│ └─ 88B4 でカバーできるか検証 → パターン 3
└─ どれも当てはまらない → セクション7「解決しない場合の相談先」を参照
実装者視点の注意点: パターン 2 と 3 を組み合わせるケースがありますが、その場合でも Info.plist に登録するコードの総数が上限を超えないことの確認は必須です。動的フィルタでセッション時に渡すコード数を絞っても、Info.plist の登録数そのものが上限を超えていると iOS がアプリ起動時に問題を起こす可能性があります。
実装後の動作検証手順
登録数を修正したら、iOSバージョンごとに実機で検出時間と成功率を計測し、上限起因の劣化がないことを定量的に確認してください。「動いた気がする」という感覚的な確認は、端末差異や電波環境の違いで結果が変わるため、再現性のある検証手順を事前に設計することが重要です。
検証環境の条件統一で押さえる5項目
検証結果の信頼性は、条件の統一度で決まります。以下の5項目を固定した上で計測を行ってください。
| 項目 | 統一すべき内容 | 補足 |
|---|---|---|
| 1. 端末 | 機種とiOSバージョンを固定 | 例: iPhone 15 / iOS 17.5.1 |
| 2. カード | 対象システムコードを持つ実カードを使用 | エミュレータでの代替は不可 |
| 3. 試行回数 | 同一条件で最低10回計測 | 外れ値除去のため |
| 4. 環境 | 金属面・他NFCデバイスから30cm以上離す | 干渉を排除する |
| 5. セッション | 毎回NFCReaderSessionを破棄・再生成する | セッション状態を初期化する |
項目5は特につまずきやすい箇所です。前回のセッションオブジェクトを使い回すと、内部状態が残ったまま次のポーリングに入るため、登録数を修正しても改善が見えにくくなることがあります。毎試行でセッションを再生成するコードパスをテスト専用のユーティリティとして切り出しておくと、検証のたびに手順がぶれません。
検出時間を指標にした合否判定の考え方
検出時間とは、NFCTagReaderSessionのtagReaderSession(_:didDetectTags:)デリゲートが呼ばれるまでの経過時間を指します。この値が安定しているかどうかが、登録数起因の問題が解消されたかどうかを判断する最も直接的な指標です。
合否判定の基準は、登録数を修正する前後で以下の変化を確認することです。
- 検出成功率の比較: 10回試行のうち何回
didDetectTagsが呼ばれたかを修正前後で比較する。修正前に5割を下回っていた場合、修正後に9割以上に回復すれば登録数が原因だったと判断できます。 - 検出時間の分散: 10回の計測値の標準偏差が修正後に小さくなっているかを確認する。上限超過時は試行ごとに検出時間が大きくばらつく傾向があります。
- エラーコードの頻度:
tagReaderSession(_:didInvalidateWithError:)に渡されるエラーを修正前後でログに残し、NFCReaderError.readerSessionInvalidationErrorSessionTimeoutの発生頻度が減っているかを見る。
実装者視点の注意点: 登録数を削減しても検出成功率が改善しない場合、セクション3で説明したエンタイトルメントファイルとInfo.plistの記述が一致していない、またはワイルドカードと固定コードの混在に起因する別の問題が残っている可能性があります。症状が変わらず続く場合は、次のセクション「iOS バージョンアップ時に備えた運用設計」で触れるモニタリング手法と合わせて、原因の切り分けを継続してください。
それでも解決しない場合の相談先
設定を修正しても読み取りが不安定な場合、システムコード以外の要因が複合している可能性があります。代表的な原因として、NFCセッションの管理ミス、通信タイムアウトの設定、端末固有の挙動の3つが挙げられます。
原因が複合しているときに確認すべき項目
まずセクション1のチェックリストに再度立ち返り、「システムコード登録数が上限内に収まっていること」を改めて確認してください。その上で以下の項目を順番に確認します。
- NFCセッションの再利用 同一の
NFCTagReaderSessionオブジェクトを複数回のポーリングに使い回していないか確認してください。セッションは1回の読み取りごとに破棄して再生成するのが基本です。使い回すとタグ検出の完了ハンドラが二重に呼ばれ、意図しない状態になります。 - タイムアウト値の設定 セッション生成時に渡すタイムアウト(
invalidateAfterFirstReadの設定)が要件と合っているか確認してください。複数タグを連続して読む構成では、適切なタイムアウト設計が必要です。 - 端末固有の挙動 同じコードでも端末モデルやiOSマイナバージョンによって挙動が異なるケースがあります。セクション6で説明した検証手順に従い、複数機種での実測値を比較してください。
- エンタイトルメントとInfo.plistの不一致 セクション3で説明したとおり、2つのファイル間でシステムコードの配列内容が1文字でも異なるとセッション初期化時にエラーが発生します。再度内容を突き合わせてください。
- Near Field通信のハードウェア制限 iPhone 7以前の機種はFeliCaのポーリングをサポートしていません。対象端末がFeliCa対応であることを確認してください(iPhone 7以降、日本向けモデル)。
自社での調査が難しい場合
Core NFCとFeliCaの組み合わせは、Appleの公開ドキュメントだけでは補えない実装上のはまりどころが多く存在します。上記5項目を確認しても解消しない場合、以下のような状況であれば外部への相談を検討する段階といえます。
- 登録数を上限以内に修正済みであるにもかかわらず、特定のカード種別だけ検出できない
- 実機検証で成功率が機種によって大きく乖離する
NFCReaderErrorのエラーコードが変動し、再現条件を特定できない
当社(株式会社ノーティックラボ)では、RAGやFine-tuningに加え、NFC実装を含むモバイルDXの技術支援を行っています。代表のAIエンジニアが実装者として直接コードレビューを行うため、「コンサルに依頼したが実装は別の会社」という体制では出てこない具体的な指摘が可能です。