iPhone で電子マネー（楽天Edy、nanaco、WAON）を読み取ってみよう！

前回の記事は [iPhone で運転免許証を読み取ってみよう！【ライブラリを使わずに350行の Swift で本籍を読み取る】](https://qiita.com/treastrain/items/58e69a1d82dccde48770) で、[前々回](https://qiita.com/treastrain/items/9025000acec37f77134a)とは異なりライブラリなしで運転免許証を読み取る方法を紹介しました。

しかし、運転免許証は個人情報の塊、かつ暗証番号は絶対に間違えてはいけないので気軽に試せない…。
今回は電子マネー（楽天Edy、nanaco、WAON）を読み取っていきます。運転免許証に比べては簡単に、お手軽に試せますね！

# 環境
- 開発
    - Xcode Version 11.2.1 (11B500)
    - Apple Swift version 5.1.2 (swiftlang-1100.0.278 clang-1100.0.33.9)
    - macOS Catalina 10.15.1（19B88）
- 実機
    - iPhone 11 Pro (A2215、MWCC2J/A)
    - iOS 13.2.3 (17B111)

# 実行結果
<img width="441" alt="スクリーンショット 2019-12-11 1.30.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/d5e3e8ce-5d3d-27c8-acd2-459234475173.png">

# ライブラリを使う場合
[前々回の記事](https://qiita.com/treastrain/items/9025000acec37f77134a)で紹介した [treastrain/TRETJapanNFCReader](https://github.com/treastrain/TRETJapanNFCReader) では 楽天Edy、nanaco、WAON などの電子マネーの読み取りも行うことができます。
Info.plist と Capability と Entitlements の設定を済ませ、ライブラリを導入してコードを数行書くけば簡単に残高や利用履歴を取得できます。

```swift:treastrain/TRETJapanNFCReaderを使った場合
import UIKit
import TRETJapanNFCReader
class ViewController: UIViewController, FeliCaReaderSessionDelegate {

    var reader: RakutenEdyReader!

    override func viewDidLoad() {
        super.viewDidLoad()
        self.reader = RakutenEdyReader(viewController: self)
        self.reader.get(itemTypes: [.balance])
    }

    func feliCaReaderSession(didRead feliCaCard: FeliCaCard) {
        let rakutenEdyCard = feliCaCard as! RakutenEdyCard
        let balance = rakutenEdyCard.data.balance! // カード残高
    }
}
```
※ [treastrain/TRETJapanNFCReader/README.md](https://github.com/treastrain/TRETJapanNFCReader) より

しかし、せっかくの Core NFC Advent Calendar なので、以降はライブラリなし、直接 Core NFC を操作して電子マネーの残高を読み取っていきます。

# Xcode プロジェクトの作成
まずは Create a new Xcode project から iOS の Single View App を作成します。

# Capability と Entitlements の設定
Project の Target から Signing & Capabilities を選択し、「+ Capability」から「Near Field Communication Tag Reading」を追加します。
<img width="976" alt="スクリーンショット 2019-12-11 0.41.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/50370c92-e733-4002-2223-edd0714300ec.png">
すると、「\<Product-Name>.entitlements」というファイルが追加されているので、「Near Field Communication Tag Reader Session Formats」に「NFC Data Exchange Format」および「NFC tag-specific data protocol」があることを確認します。

# Info.plist の設定
Info.plist に「Privacy - NFC Scan Usage Description」と「ISO18092 system codes for NFC Tag Reader Session」を追加します。
「Privacy - NFC Scan Usage Description」には NFC を何のために使用するのかについての説明を、「ISO18092 system codes for NFC Tag Reader Session」の配下には以下の FeliCa システムコードを記述します。

- `FE00`

<img width="599" alt="スクリーンショット 2019-12-11 0.45.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/22bff95f-0a31-e4fa-b08c-7f6ea67cbd09.png">
以上で設定はおしまいです。

# ファイルツリー
今回のプロジェクトのファイルツリーを確認しておきます。
<img width="271" alt="スクリーンショット 2019-12-11 0.48.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/438c31e9-7f6d-7585-7be6-8fd4b4f6fd39.png">
処理は全て `ViewController.swift` に記述します。

# コーディング
まずコード全文を載せた後、各部を解説していきます。[**運転免許証のときに比べればネストの深さはかわいいものです。**](https://twitter.com/shu223/status/1203879837112270848)

```swift:ViewController.swift
import UIKit
import CoreNFC

class ViewController: UIViewController, NFCTagReaderSessionDelegate {
    
    var session: NFCTagReaderSession?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        guard NFCTagReaderSession.readingAvailable else {
            print("NFC タグの読み取りに非対応のデバイス")
            return
        }
        
        self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
        self.session?.alertMessage = "カードの上に iPhone の上部を載せてください"
        self.session?.begin()
    }
    
    func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
        print("tagReaderSessionDidBecomeActive(_:)")
    }
    
    func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
        let readerError = error as! NFCReaderError
        print(readerError.code, readerError.localizedDescription)
    }
    
    func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
        print("tagReaderSession(_:didDetect:)")
        
        let tag = tags.first!
        session.connect(to: tag) { (error) in
            if let error = error {
                session.invalidate(errorMessage: error.localizedDescription)
                return
            }
            
            guard case NFCTag.feliCa(let feliCaTag) = tag else {
                session.invalidate(errorMessage: "FeliCa ではない")
                return
            }
            
            session.alertMessage = "カードを読み取っています…"
            
            let idm = feliCaTag.currentIDm.map { String(format: "%.2hhx", $0) }.joined()
            print("IDm:", idm)
            
            /// FeliCa サービスコード
            let rakutenEdyServiceCode = Data([0x13, 0x17].reversed())
            let nanacoServiceCode = Data([0x55, 0x97].reversed())
            let waonServiceCode = Data([0x68, 0x17].reversed())
            let serviceCodeList = [rakutenEdyServiceCode]
            
            /// ブロック数
            let blocks = 1
            let blockList = (0..<blocks).map { (block) -> Data in
                Data([0x80, UInt8(block)])
            }
            
            feliCaTag.readWithoutEncryption(serviceCodeList: serviceCodeList, blockList: blockList) { (status1, status2, blockData, error) in
                if let error = error {
                    session.invalidate(errorMessage: error.localizedDescription)
                    return
                }
                
                guard status1 == 0x00, status2 == 0x00 else {
                    print("ステータスフラグがエラーを示しています", status1, status2)
                    session.invalidate(errorMessage: "ステータスフラグがエラーを示しています s1:\(status1), s2:\(status2)")
                    return
                }
                
                let data = blockData.first!
                let balance = data.toIntReversed(0, 3)
                
                print(data as NSData)
                print("残高: ¥\(balance)")
                session.alertMessage = "残高: ¥\(balance)"
                session.invalidate()
            }
        }
    }
}

extension Data {
    /// https://github.com/treastrain/TRETJapanNFCReader/blob/master/TRETJapanNFCReader/Extensions.swift#L112
    func toIntReversed(_ startIndex: Int, _ endIndex: Int) -> Int {
        var s = 0
        for (n, i) in (startIndex...endIndex).enumerated() {
            s += Int(self[i]) << (n * 8)
        }
        return s
    }
}
```

# コード解説
## 対応端末かどうかを確かめる
```swift
guard NFCTagReaderSession.readingAvailable else {
    print("NFC タグの読み取りに非対応のデバイス")
    return
}
```
前回に引き続き Core NFC を使うときのお約束、そもそも使っている端末が読み取りに対応しているかどうかを `NFCTagReaderSession.readingAvailable` で確かめます。

## `NFCTagReaderSession` をスタートさせる
```swift
self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
self.session?.alertMessage = "カードの上に iPhone の上部を載せてください"
self.session?.begin()
```
対応端末であることを確認した後、`NFCTagReaderSession` を初期化して `begin()` でスタートさせます。読み取るカードは FeliCa、つまり ISO 18092 なので、`pollingOption` もそのように指定します。

## `NFCTagReaderSessionDelegate`
```swift
func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
    print("tagReaderSessionDidBecomeActive(_:)")
}

func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
    let readerError = error as! NFCReaderError
    print(readerError.code, readerError.localizedDescription)
}

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
    print("tagReaderSession(_:didDetect:)")
    
    // …
}
```
この部分の説明は[前回の記事と全く同じ](https://qiita.com/treastrain/items/58e69a1d82dccde48770#nfctagreadersessiondelegate)なので省略します。

## タグに接続する
```swift
let tag = tags.first!
session.connect(to: tag) { (error) in
    if let error = error {
        session.invalidate(errorMessage: error.localizedDescription)
        return
    }
    
    guard case NFCTag.feliCa(let feliCaTag) = tag else {
        session.invalidate(errorMessage: "FeliCa ではない")
        return
    }
    
    session.alertMessage = "カードを読み取っています…"
    
    let idm = feliCaTag.currentIDm.map { String(format: "%.2hhx", $0) }.joined()
    print("IDm:", idm)
    
    // …
}
```
まずは `session.connect(to:completionHandler:)` でタグに接続します。
接続したタグが FeliCa であるかを確かめ、そうでない場合は `session.invalidate(errorMessage:)` でセッションを終了します。
ついでに FeliCa タグに割り振られている IDm くらいは `print` しておきましょうか。
今回は FeliCa システムコードを `FE00` のみ Info.plist に記述していますので、`feliCaTag.currentIDm` は必ず `FE00` の IDm になります。

## FeliCa サービスコードの指定
```swift
/// FeliCa サービスコード
let rakutenEdyServiceCode = Data([0x13, 0x17].reversed())
let nanacoServiceCode = Data([0x55, 0x97].reversed())
let waonServiceCode = Data([0x68, 0x17].reversed())
let serviceCodeList = [rakutenEdyServiceCode]

/// ブロック数
let blocks = 1
let blockList = (0..<blocks).map { (block) -> Data in
    Data([0x80, UInt8(block)])
}
```
さて、FeliCa ですがデータ構造を少し理解しておく必要があります。
公式の情報であれば [FeliCaカード ユーザーズマニュアル 抜粋版](https://www.sony.co.jp/Products/felica/business/tech-support/)、Qiita の記事であれば [[PASMO] FeliCa から情報を吸い出してみる - FeliCaの仕様編 [Android][Kotlin]](https://qiita.com/YasuakiNakazawa/items/3109df682af2a7032f8d#felica-のデータ構造について) がとてもわかりやすいと思います。

*やはり Android の情報にたどりつく…*

次の Read Without Encryption コマンドを送信するために、サービスコードをリトルエンディアンで準備しておきます。

## Read Without Encryption コマンドを送信
```swift
feliCaTag.readWithoutEncryption(serviceCodeList: serviceCodeList, blockList: blockList) { (status1, status2, blockData, error) in
    if let error = error {
        session.invalidate(errorMessage: error.localizedDescription)
        return
    }
    
    guard status1 == 0x00, status2 == 0x00 else {
        print("ステータスフラグがエラーを示しています", status1, status2)
        session.invalidate(errorMessage: "ステータスフラグがエラーを示しています s1:\(status1), s2:\(status2)")
        return
    }
    
    // …
}
```
Read Without Encryption コマンドを FeliCa タグに送信します。FeliCa のステータスフラグについては [FeliCaカード ユーザーズマニュアル 抜粋版](https://www.sony.co.jp/Products/felica/business/tech-support/) に記載があります。両方とも `0` なら正常に終了しています。
もし、`status1` が `1`、`status2` が `166` (`0xA6`) であれば "サービスコードリスト不正" にあたるので、前項のサービスコードの指定が間違っていることになります。`serviceCodeList` の中身が読み取ろうとしたカードに合っていますか？

## データの解析
```swift
let data = blockData.first!
let balance = data.toIntReversed(0, 3)

print(data as NSData)
print("残高: ¥\(balance)")
session.alertMessage = "残高: ¥\(balance)"
session.invalidate()
```
エラーなく Read Without Encryption コマンドが終了したら、いよいよデータの解析です。
解析をしやすいように `NSData` で `print` しました。

…と、言っても今回の 楽天Edy、nanaco、WAON の残高は先頭4ビットがリトルエンディアンで入っているので、それを `Int` にします。`toIntReversed` は以下の `extension` に定義しました。

```swift
extension Data {
    /// https://github.com/treastrain/TRETJapanNFCReader/blob/master/TRETJapanNFCReader/Extensions.swift#L112
    func toIntReversed(_ startIndex: Int, _ endIndex: Int) -> Int {
        var s = 0
        for (n, i) in (startIndex...endIndex).enumerated() {
            s += Int(self[i]) << (n * 8)
        }
        return s
    }
}
```

すべて終わったら忘れずに `session.invalidate()` を呼んでセッションを正しく終了させましょう。

# 後記
お疲れさまでした。前回のコード量の3分の1です。
とても簡単に残高を取得できました。
Core NFC、どんどん使っていきましょう…！

[ここでのコードは Gist にも載せておきました。](https://gist.github.com/treastrain/f4c4b0e8e0ea2c874d90cd07597549e4)参考にしてください。