FeliCa が遅いしフルスキャンも不可能 後編【iOS 13 Core NFC】

iOS 13 Core NFC での FeliCa に関する制限を紹介した[前編](https://qiita.com/treastrain/items/e8dc5084a663f09ff469)、今回はそれの続きです。

# iOS での FeliCa の制限
- FeliCa の読み取りで得られる最初の `currentSystemCode` は Info.plist での FeliCa システムコードの**記述する順番に依存**する → [前編で紹介](https://qiita.com/treastrain/items/e8dc5084a663f09ff469)
- FeliCa の読み取り速度は Info.plist での FeliCa システムコードの**記述する順番に依存**する → [前編で紹介](https://qiita.com/treastrain/items/e8dc5084a663f09ff469)
- FeliCa のフルスキャンを iOS で行いたい場合、「じゃあ Info.plist に存在する全ての FeliCa システムコードを記載すればいいのでは…？」と考えつくが、**それは現実的な解決策ではない**

# 対応する FeliCa システムコードを増やすと最悪どれくらい遅くなるのか
[前編](https://qiita.com/treastrain/items/e8dc5084a663f09ff469)では

> iOS 13 の Core NFC の場合は**検出された FeliCa カードにあるシステムのうち、Info.plist に記載され 最も順番が前にある FeliCa システムコードに一致するシステム** が検出、`currentSystemCode` および `currentIDm` にはそれが入ることになります。

> そのため、多数の FeliCa システムコードに対応しようとした場合、**Info.plist に記載する順番が非常に重要になります。**

と記事の最後に述べました。では具体的にどれくらい速度に変化があるのでしょうか。実際に測定してみます。

# ここでの "読み取り速度" の定義
そもそも FeliCa は NFC の中で読み取り速度がめちゃ速いです。NFC-B の運転免許証の読み取りに比べても速度が段違いです。

しかし、iOS での Core NFC で私が述べたい "速度" はこのことではありません。

※[事例紹介をスキップして早く本題に行きたい方はこちらからジャンプ](#%E6%A4%9C%E8%A8%BC--felica-%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E5%A2%97%E3%82%84%E3%81%99%E3%81%A8%E3%81%A9%E3%82%8C%E3%81%A0%E3%81%91%E9%81%85%E3%81%8F%E3%81%AA%E3%82%8B%E3%81%8B-)

## 各 App での読み取り時間に関する表示
iOS 13 のリリース以降、多くの電子マネーカードリーダー系の App が登場しましたが、各 App のスキャン画面等にこんな記載がありませんか？

|[Japan NFC Reader](https://apps.apple.com/jp/app/id1480265213)|[ICリーダー](https://apps.apple.com/jp/app/id1474445461)|[CardPort](https://apps.apple.com/jp/app/id1478284681)|
|:----------------:|:-----------------:|:------------------:|
|![IMG_6487.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/0682acc5-5d5a-32a6-6a00-8a940d4d8b02.jpeg)|![IMG_6486.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/78f54099-140b-b106-ece2-726703a3cb37.jpeg)|![IMG_6485.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/ed522673-d63d-ef4c-8e59-4074a6156f27.jpeg)|
|読み取りに時間がかかる場合があります。|読み込みまで少し時間がかかることがあります。|読み取りには時間がかかることがあります。|

（個人的）ド定番 iOS カードリーダー 御三家のどれもに **読み取りに時間がかかる** という記載があります。
本記事を読んでいただくと、なぜわざわざこのように表示しているのかがわかり、また文脈としては [ICリーダー](https://apps.apple.com/jp/app/id1474445461) さんの 読み込み**まで**少し時間がかかることがある が最も正しい表現であることがわかると思います。

[Japan NFC Reader](https://apps.apple.com/jp/app/id1480265213) の場合、このあと次のような表示になります。
<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/652d8047-41c9-3636-cd20-59f4e7bc0b8a.jpeg">
**読み取り中…**
…ここで思うことはありませんか？別に「読み取り中…」の文言が表示されるなら、読み取り開始前の画面にわざわざ「読み取りに時間がかかる」ことを表示しなくてもいいんじゃないでしょうか。これは各 App 開発者の親切心で……？

……（少なからず親切心もあるでしょうが）**それは違います。**

## コードを見てみる
ここで少しコードを見てみることにしましょう。

```swift
var session: NFCTagReaderSession?

// …

func scan() {
    // …
    self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
    self.session?.alertMessage = "カードの上に iPhone の上部を載せてください。読み取りまでに時間がかかることがあります。"
    self.session?.begin()
}

// …

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    let tag = tags.first!
    session.connect(to: tag) { (error) in
        // …
        session.alertMessage = "カードを読み取っています…"
        // …
    }
}

```

App がカードのスキャンを行えるようになった後に表示されるメッセージは、`scan()` の中にある `session.alertMessage` です。そして、実際にカードを検出し、プログラム側にその情報がやってくるのが `tagReaderSession(_:didDetect:)` の中の `session.alertMessage` です。

ここで大きな問題になるのが、FeliCa カードに iPhone を載せてから `tagReaderSession(_:didDetect:)` が呼ばれるまでに、**ものすごく時間がかかる場合がある**ということです。その条件が **検出された FeliCa カードに存在するシステムが、Info.plist に記載されている FeliCa システムコードの順番の後ろの方にあるとき** です。

ここでの**読み取り速度**とは、**`tagReaderSession(_:didDetect:)` が呼ばれるまでの時間**のことを示します。

# 検証 ~ FeliCa システムコードを増やすとどれだけ遅くなるか ~
今回は以下のコードを使って、`Date`の差がどれだけ増えるかを見ていきます。

```swift
func scan() {
    // …
    self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
    self.session?.alertMessage = "カードの上に iPhone の上部を載せてください。読み取りまでに時間がかかることがあります。"
    self.session?.begin()
}

func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
    self.start = Date()
    print("tagReaderSessionDidBecomeActive(_:)")
}

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    let elapsedTime = Date().timeIntervalSince(self.start)
    print("elapsed time:", elapsedTime)
    // …
    session.alertMessage = "完了"
    session.invalidate()
}
```

対象とするカード、今回は楽天Edyを用います。このようにカードを iPhone の NFC 読み取り部をピッタリと置いた状態で Scan ボタンをタップします。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/9265106d-bd62-6e81-75f2-b48bfd2ed65b.jpeg">
`tagReaderSession(_:didDetect:)` の `print` でコンソールに出力される `elapsed time` の値がどれだけ変化するか測定します。

楽天Edy カードが持っている FeliCa システムコードは `0x8B61` と `0xFE00` です。Info.plist に記述する `0xFE00` の順番を段々と後ろにしていきます。
<img width="500" alt="スクリーンショット 2019-12-22 2.14.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/221db72c-f9af-e3ce-1dd6-fe5abe4e420d.png">
<img width="500" alt="スクリーンショット 2019-12-22 2.38.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/c3d5f12b-37a8-e6fb-33af-e63b7c9d1e46.png">

# 結果
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/c123b909-ab41-2984-fa38-4b821c6a2283.png">
このグラフのもとになった表はページ下部に記載しました。
FeliCa システムコードの順番が1つ後ろにずれると約0.3秒、`tagReaderSession(_:didDetect:)` が呼ばれるまでの時間が延びることがわかりました。

また、[`NFCReaderSession` には時間制限がある](https://developer.apple.com/documentation/corenfc/nfcreadererror/2897267-readersessioninvalidationerrorse)のですが、その45秒となる Item 148 = **149 個目**よりも FeliCa システムコードを多くしてしまうと、そもそも `tagReaderSession(_:didDetect:)` が呼ばれないことがわかりました。

各カードが持っている FeliCa システムコードはそれぞれ異なる場合があるので、1つの App で多数のカードに対応させようとすると、それだけ Info.plist に記載しなければならない FeliCa システムコードの数も増え、順番によってカードが検出されるまでの時間が左右されるのは使う側としてとても不便です。
また、`tagReaderSession(_:didDetect:)` が呼ばれないとプログラム側からもカードが iPhone に載せられているのかすら分からないため、各 App では「読み込みまで時間がかかる」という記載がスキャン前の段階で表示されることにつながっています。



# Info.plist に 存在する全ての FeliCa システムコードを記載するのは現実的な解決策ではない
もし、iOS で FeliCa のフルスキャンを行おうとしたときに、**`0x0001` から `0xFEFE` までの全ての FeliCa システムコードをInfo.plist に載せれば…？**と思いつきますが、そもそも検出のみで149個が限界なので、それはいい方法とは言えないという結論になりました。

そもそもなぜ iOS (Apple) は検出にこれほど時間をかけるのでしょうか。
「ショートカット」のオートメーションに「NFC タグ」が使えますが、これは Apple 製ということもあってか FeliCa も即検出、登録することができています。確実にサードパーティのみを締め出しているということになりますが……。[FeliCa システムの IDm による切り替えができない](https://qiita.com/treastrain/items/04f50a91f70fd6480fc0)のと合わせて、理由がわかりません。

iOS 14…ではさらなる Core NFC の機能開放を希望します…。


# 結果（表）
"Item x" の列は 0 は `0xFE00` を Item 0 に記述した場合、1 は Item 1 に記述した場合…というような感じです。Item 7 = 8個目で2秒を超え始めています。実用ではもっと早く検出されたほうがいいですよね。

| Item x | 1       | 2       | 3       | Ave.    |
| :---: | ------: | ------: | ------: | ------: |
| 0     | 0.046976  | 0.045720  | 0.045037  | 0.045911  |
| 1     | 0.349753  | 0.346604  | 0.345791  | 0.347383  |
| 2     | 0.647677  | 0.649241  | 0.646586  | 0.647835  |
| 3     | 0.947898  | 0.950009  | 0.952775  | 0.950227  |
| 4     | 1.250441  | 1.250477  | 1.250740  | 1.250553  |
| 5     | 1.550706  | 1.551337  | 1.552101  | 1.551381  |
| 6     | 1.854309  | 1.857909  | 1.855477  | 1.855898  |
| 7     | 2.156484  | 2.155968  | 2.155585  | 2.156012  |
| 8     | 2.459123  | 2.457329  | 2.458836  | 2.458429  |
| 9     | 2.759697  | 2.762735  | 2.758815  | 2.760416  |
| 10    | 3.058142  | 3.060277  | 3.059271  | 3.059230  |
| 15    | 4.566140  | 4.568311  | 4.569674  | 4.568042  |
| 20    | 6.072544  | 6.079473  | 6.078423  | 6.076813  |
| 25    | 7.583499  | 7.585000  | 7.584232  | 7.584244  |
| 30    | 9.094974  | 9.094320  | 9.096275  | 9.095190  |
| 40    | 12.111050 | 12.109540 | 12.105498 | 12.108696 |
| 50    | 15.123403 | 15.119864 | 15.120571 | 15.121279 |
| 100   | 30.202593 | 30.200774 | 30.201501 | 30.201623 |
| 101   | 30.512402 | 30.505607 | 30.501605 | 30.506538 |
| 102   | 30.811206 | 30.814952 | 30.807862 | 30.811340 |
| 103   | 31.109441 | 31.106589 | 31.107631 | 31.107887 |
| 140   | 42.270938 | 42.267985 | 42.274794 | 42.271239 |
| 145   | 43.787478 | 43.777525 | 43.775778 | 43.780260 |
| 148   | 44.683454 | 44.679594 | 44.683043 | 44.682030 |
| 149   |         |         |         | NaN     |
| 150   |         |         |         | NaN     |