iOSバージョン別の検出時間と登録上限はどう違う?
当社の実機計測では、iOS 17に約148個のシステムコード登録上限があることを確認しており、上限を超えるとポーリング時間が急増するどころか検出そのものが不可能になります。さらに、OSバージョンごとに動作原理が根本から異なるため、「登録数を減らせば速くなる」という対策が有効なケースとそうでないケースを見極めることが重要です。
iOS 16 / 17 / 18 / 26の実測データ比較
当社実測調査(2026年7月、各条件10〜20回の中央値)では、iOS 16・17・18・26の4バージョンで登録数を変えながら検出時間を計測しました。結果は以下のとおりです。
| OSバージョン | テスト端末 | 登録10個 | 登録50個 | 登録145〜150個 | 登録200個以上 |
|---|---|---|---|---|---|
| iOS 16.7.5 | iPhone 8 | 2.78秒 | 約15秒 | 約45秒 | 60.8秒(200個) |
| iOS 17.4.1 | iPhone XR | 0.031秒 | 約15秒 | 44.3秒(145個)/ 検出不可(150個) | 測定不可 |
| iOS 18.7.8 | iPhone 13 | 0.054秒 | 0.054秒 | 0.054秒 | 0.054秒(500個まで確認) |
| 26.5.2 | iPhone 13 | 0.054秒 | 0.054秒 | 0.054秒 | 0.054秒(500個まで確認) |
出典: 当社実測(各機種・各OSバージョン、N=10〜500、各条件10〜20回の中央値、2026年7月)
iOS 16は登録数に比例して検出時間が線形に増加し、1コードあたり約0.3秒が加算されます(線形性±1%以内)。登録数が200個に達すると検出時間は60.8秒に達し、デフォルトのタイムアウト(60秒)に接触する水準です。
iOS 17は少数登録時だけ高速で、登録10個では0.031秒と非常に高速です。しかし50個を超えるとiOS 16と同じ線形動作に切り替わります。iOS 18以降は登録数に関わらず約0.054秒で一定であり、500個を登録しても変化はありませんでした。
計測上の注意点: FeliCaの検出はシステムコードを先頭から逐次ポーリングするため、目的のコードがリストのどこに位置するかで検出時間が変わります。この計測では最悪ケースを評価するために、目的コードをリストの末尾に配置して計測しています。
iOS 17の上限到達時に起きる挙動
iOS 17固有の問題は、「遅くなる」のではなく「検出が完全に不可能になる」点にあります。
当社実測では、iPhone XR(iOS 17.4.1)にシステムコードを150個登録した状態でFeliCaカードをスキャンしたところ、端末を再起動した後も同一の条件で検出不可が再現しました(出典: 当社実測、2026年7月)。145個登録時は44.3秒で検出を確認しているため、148個前後に上限が存在すると判断しています。
この挙動はタイムアウトエラー(NFCReaderError.readerSessionInvalidationErrorSessionTimeout)とは異なります。セッションを長時間維持しても結果は変わらず、個数そのものが原因です。iOS 16には同様の上限が確認されておらず、200個でも60.8秒で読み取れています。
実務上の対処方針を整理すると次のとおりです。
| 対象OSの下限 | 登録数の上限目安 | 根拠 |
|---|---|---|
| iOS 16 | 約10個 | 10個で2.78秒。これ以上増やすと許容レスポンスを超えやすい |
| iOS 17 | 148個未満 | 148個超で検出不可。かつ50個超から線形遅延が始まる |
| iOS 18以降のみ | 事実上制限なし | 500個でも0.054秒で一定(当社実測範囲内) |
iOS 17対応が必要なアプリでは、システムコードの絞り込みが単なる最適化ではなく動作保証の要件になります。具体的な絞り込み手順と記述方法は次のセクションで解説します。
まず確認すべき原因はどこにある?
多くの場合、Info.plistに登録するシステムコード数の過多、または書き方の誤りが読み取り遅延の直接原因です。端末の不具合やNFCアンテナの問題と混同されがちですが、設定ファイルの見直しだけで改善するケースが大半です。
Core NFCがFeliCaを検出する仕組み
Core NFCとは、iOSが提供するNFC読み取りフレームワークです。FeliCa読み取りでは、NFCTagReaderSessionが起動すると、iOSはInfo.plistに記載されたシステムコードのリストを参照し、上から順にポーリング(タグへの問い合わせ)を実行します。
この逐次的なポーリングシーケンスが、遅延の根本的なメカニズムです。登録コード数が多いほどシーケンスが長くなり、検出までの時間が伸びます。カードが最後のコードに対応している場合、リスト全体を走査してから初めて検出が成立します。
検出の流れを整理すると、次のとおりです。
NFCTagReaderSessionをpollingOption: [.iso18092]で開始する- iOSがInfo.plistの
com.apple.developer.nfc.readersession.felica.systemcodesを読み込む - 登録されたシステムコードを先頭から順にポーリングする
- カードが応答したコードで
NFCFeliCaTagインスタンスが生成される tagReaderSession(_:didDetect:)デリゲートが呼ばれ、アプリ側の処理が始まる
ステップ3の反復回数が多いほど、ステップ4の到達が遅くなります。コード数を絞ることが最初の改善策になる理由はここにあります。
遅延・タイムアウト・失敗の症状別チェックリスト
症状によって調査すべき箇所が異なります。以下を入口に、原因を絞り込んでください。
| 症状 | 主な原因の候補 | 最初に確認する箇所 |
|---|---|---|
| カードをかざしてから検出まで2〜5秒かかる | systemCodesの登録数が多い | Info.plistのコード数 |
| 一定時間後に毎回タイムアウトする | 登録上限超過、または対象コードが未登録 | iOSバージョンと登録数の組み合わせ |
| 特定の端末だけ失敗する | OSバージョン差による挙動の違い | iOSバージョン(詳しくは後述します) |
| カードの種類によって成否が分かれる | ワイルドカード指定の誤りまたは未使用 | 88B4の記述有無 |
| アプリ再起動後は読めるが、連続使用で失敗する | invalidationHandlerの未処理 |
セッション再生成の実装 |
| ビルド直後は動くが実機で失敗する | Entitlementsの設定漏れ | .entitlementsファイルとプロビジョニング |
症状が複数重なる場合、まずシステムコード数を削減してから再検証することを推奨します。単一の変数を変えて計測することが、原因特定を早めます。なお、iOSバージョンごとの検出時間の実測値と登録上限については後述のセクションで詳しく取り上げます。
Info.plistを正しく設定する手順
スキャン対象を最小限のシステムコードに絞り込んだうえで、Entitlementsファイルとの整合性を取ることが最短の改善策です。手順を3ステップに分けて説明します。
ステップ1: 必要なシステムコードを洗い出す
つまずきやすい点: 「念のため」で追加したコードが遅延の根本原因になっていることが多いです。
まず、アプリが実際に読み取るFeliCaカード・デバイスの種別を整理します。下表を使って、対象システムコードを棚卸ししてください。
| カード・サービス例 | 代表的なシステムコード | 本当に必要か |
|---|---|---|
| Suica / PASMO(交通系) | 0003 | 実装要件次第 |
| 楽天Edy | 8008 | 実装要件次第 |
| nanaco | 88B4(ワイルドカード) | 実装要件次第 |
| 独自FeliCaカード | 任意(要確認) | 仕様書で確認 |
洗い出しの手順は以下のとおりです。
- 仕様書またはカード発行元ドキュメントで、読み取るサービスのシステムコードを確認する。(所要時間の目安: 30分〜1時間)
- 一覧にしたコードのうち、現在リリース中の機能で実際に使っているものだけを残す。使っていないコードは削除する。
- 将来機能のためのコードは Info.plist には含めない。機能追加時にアプリ更新で追加するほうが安全です。
ワイルドカード指定(88B4)の扱いは、スキャン対象が広範になりiOS 16・17では遅延を招きやすいため、避けられる場合は個別コードに置き換えることを検討してください。ワイルドカードの詳細な注意点は別セクション「Info.plistのsystemCodesとは何か?」を参照してください。
ステップ2: Info.plistへの記述と型・形式の確認
つまずきやすい点: 値の型がStringでなくNumberになっていたり、16進数表記の大文字・小文字が混在したりするケースが現場で頻出します。
洗い出したシステムコードを Info.plist に記述します。キー名と型、書き方の要件を以下にまとめます。
| 項目 | 正しい設定 |
|---|---|
| キー名 | com.apple.developer.nfc.readersession.felica.systemcodes |
| 型 | Array(各要素はString) |
| 値の形式 | 4桁の16進数(大文字・小文字どちらも可だが、プロジェクト内で統一する) |
| 先頭の「0x」 | 不要。0003のように数字のみ記述する |
Xcodeのソースエディタ(plist XML)で記述する場合の例は次のとおりです。
<key>com.apple.developer.nfc.readersession.felica.systemcodes</key>
<array>
<string>0003</string>
<string>8008</string>
</array>
記述後に確認すべき点をリストアップします。
- Xcode左ペインでファイルを選択し、Property List形式で表示したときに各要素の型が「String」になっているか確認する。
- 登録するコードの総数を数える。iOS 16・17を対象に含む場合は、不要なコードが混入していないかもう一度確認する(iOSバージョン別の上限の詳細はセクション3を参照)。
- XMLを直接編集した場合は
plutil -lint Info.plistでシンタックスエラーがないか確認する。(所要時間の目安: 5分)
ステップ3: Entitlementsとの整合性チェック
つまずきやすい点: Info.plist に正しく書いても、Entitlementsファイルの com.apple.developer.nfc.readersession.formats に MIFARE や FeliCa が含まれていないと、実行時にセッションが即時終了します。
Info.plist の設定はEntitlementsファイルと対になって機能します。整合性を確認する手順は以下のとおりです。
<プロジェクト名>.entitlementsを開き、com.apple.developer.nfc.readersession.formatsキーが存在するか確認する。(所要時間の目安: 2分)- そのArrayに
MIFAREまたはFeliCaが含まれているか確認する。FeliCa専用アプリであればFeliCaのみで構いません。 - Apple Developer Portalでアプリの App ID に「NFC Tag Reading」ケーパビリティが有効になっているか確認する。Xcodeの「Signing & Capabilities」タブからも確認できます。
- 変更後はクリーンビルド(
Cmd + Shift + K)を実行してから実機でテストする。シミュレータはNFCを非サポートのため、必ず実機を使う。
下表で Info.plist とEntitlementsの対応関係を整理します。
| 設定ファイル | キー | 役割 |
|---|---|---|
| Info.plist | com.apple.developer.nfc.readersession.felica.systemcodes |
ポーリング対象のシステムコードを列挙する |
| Entitlements | com.apple.developer.nfc.readersession.formats |
NFCセッションで使用するタグ形式を宣言する |
| Apple Developer Portal | NFC Tag Reading ケーパビリティ | 実機実行の権限をプロビジョニングプロファイルに含める |
3つすべてが揃って初めてFeliCaの読み取りセッションが正常に開始します。どれか1つでも欠けると、NFCReaderError が即時返却されます。エラーコードの分類と invalidationHandler の実装パターンは、次のセクション「実装でハマりやすい技術的落とし穴」で詳しく取り上げます。
実測で性能差を検証するときの条件設定方法
OSバージョン間の比較を有意にするには、デバイス機種・NFCアンテナ距離・ループ回数を統一した条件下で計測する必要があります。条件が揃っていない実測データは「端末の個体差ではないか」という反論で結論が崩れます。
当社が実測時に揃えた条件と制約事項
当社のFeliCa検出時間調査(2026年7月)では、以下の設計で計測を行いました。
| 項目 | 採用した設計 | 意図 |
|---|---|---|
| 計測区間 | セッション開始からカード検出まで | NFCスタック全体の遅延を捉える |
| タイマ精度 | モノトニッククロックでミリ秒まで | OS時刻の揺れを排除する |
| 使用カード | 全条件で同一の1枚を使用 | カード個体差を排除する |
| 検索対象コードの位置 | リストの末尾に固定 | 逐次探索の最悪ケースで評価する |
| 試行回数 | 条件あたり10〜20回 | 偶発的なタイムアウトをサンプルで希釈する |
| 集計方法 | 平均ではなく中央値 | タイムアウト値が平均を外れ値方向に引き上げるのを防ぐ |
目的のシステムコードをリスト末尾に固定する点は特に重要です。先頭に置けば「たまたま速く検出できた」条件を計測することになり、登録数が与える実際のコストを過小評価します。
結果をハードウェア差で説明されないための手当て
最も効果的だったのは、iOS 18と26の比較を同一端末のOSアップデート前後で行ったことです。同一ハードウェアでOSだけを変数にできるため、「端末が違うだけ」という反論を完全に排除できます。
iOS 16と17については同一端末でのアップデート比較が難しく、別端末で計測せざるをえませんでした。この場合、線形域の「1システムコードあたりの検出速度」が両端末で一致していることを確認し、観測された差がハードウェア差では説明できないことを示す間接的な根拠としました。
外挿によって推定した値には実測値と区別できる表記を付け、未実測であることを明記しています。正直に書いていない数値は、後から反証されたときに記事全体の信頼性を毀損します。
また、当社の実測には以下の制約があります。事前に把握したうえでデータを読んでください。
- 対象外のOSバージョン(例: iOS 15以前)については計測していない
- 単一システムコードを持つカードのみで検証しており、マルチシステムカードは対象外
- アンテナ位置の揺れはオペレータの習熟で抑制したが、機械的な固定は行っていない
測定していないことを測定したように書かないことが、実測レポートの信頼性を最終的に決めます。これらの制約を踏まえたうえで、次のセクションの改善後チェックリストと組み合わせて活用してください。
改善後のパフォーマンス検証チェックリスト
設定変更後は、最低3パターンのシステムコード数で実機計測し、タイムアウト閾値(デフォルト60秒)に対する余裕率を確認してください。
Info.plistの修正が完了しても、その変更が実際に効いているかを数値で確認しなければ作業は完結しません。以下のチェックリストは、変更前後で計測値を比較するための最小手順です。
検証前に確認する準備項目
| 確認項目 | 合格条件 |
|---|---|
| ビルド設定 | Release構成でアーカイブ済み(Debug構成は挙動が異なる場合がある) |
| 計測コードの挿入 | CFAbsoluteTimeGetCurrent() をセッション開始・タグ検出の直前後に配置済み |
| 計測ループ数 | 同一条件で最低20回の読み取りを実施する設定になっている |
| 外れ値の記録方針 | 最大値・最小値を除外せず、中央値と標準偏差を別途記録する |
| 制御外項目の明記 | 計測ログに端末機種・OSバージョン・計測日時を自動付記している |
3パターン計測の実施手順
変更後の検証では、システムコード数を段階的に変えた3パターンで実機計測してください。
- 最小構成(実運用に必要なコードのみ): 本番リリース想定の最終形。この値がベースラインになります。
- 変更前と同数: 修正前のコード数を再現した構成。改善幅の基準値として使います。
- 中間値(1と2の中間程度のコード数): 削減効果が線形かどうかを確認するための中間点です。iOSバージョンによって線形性が異なることは前セクションで触れたとおりです。
計測値はパターンごとに中央値を算出し、以下の形式でまとめてください。
| パターン | 登録コード数 | 中央値(秒) | タイムアウト余裕率 |
|---|---|---|---|
| 最小構成 | 記入 | 記入 | 記入 |
| 中間値 | 記入 | 記入 | 記入 |
| 変更前と同数 | 記入 | 記入 | 記入 |
タイムアウト余裕率は (60 - 中央値) / 60 × 100 で算出します。余裕率が80%未満の場合は、さらなるコード数削減を検討してください。
合格・要再検討の判断基準
以下のいずれかに該当する場合は、設定を見直す必要があります。
- 最小構成でも中央値が8秒を超える(ユーザが実用上ストレスを感じる目安です)
- パターン間で中央値の差がほぼゼロ(iOS 18以降の特性としてあり得る。原因切り分けが必要)
- 20回の計測中に1回以上タイムアウトが発生する
iOS 18以降では登録コード数を減らしても検出時間がほぼ変わらない特性があります。この場合、遅延の原因はInfo.plist設定以外(セッション生成コストやUIスレッドの競合等)にある可能性が高いため、計測対象を切り替えて再調査してください。
本番リリース前の最終確認リスト
[ ] 3パターン計測が完了し、結果を表形式で記録した
[ ] 最小構成でのタイムアウト余裕率が80%以上である
[ ] iOS 16 / 17 / 18 それぞれの実機で計測を実施した
[ ] invalidationHandler のエラーログに読み取り失敗が記録されていない
[ ] Entitlementsとシステムコード一覧の最終整合確認が済んでいる
[ ] 計測コード(CFAbsoluteTimeGetCurrent)はリリースビルドから除外している
計測コードをリリースビルドに含めたまま提出するミスは現場で繰り返されます。#if DEBUG フラグで確実に囲ってください。
