iPhone で交通系IC（Suica、PASMO、ICOCA、…etc.）を読み取ってみよう！

[運転免許証](https://qiita.com/treastrain/items/58e69a1d82dccde48770)、[物販向け電子マネー（楽天Edy、nanaco、WAON）](https://qiita.com/treastrain/items/f5c518486d19ba3573d3)に引き続き、今回は交通系ICである Suica、PASMO、ICOCA などの残高を iPhone で読み取ってみましょう！

# 環境
- 開発
 - Xcode Version 11.3 (11C29)
 - Apple Swift version 5.1.3 (swiftlang-1100.0.282.1 clang-1100.0.33.15).
 - macOS Catalina 10.15.2（19C57）
- 実機
 - iPhone 11 Pro (A2215、MWCC2J/A)
 - iOS 13.3 (17C54)

# 実行結果
<img width="443" alt="スクリーンショット 2019-12-15 0.17.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/cea798af-867a-4b62-3875-8e25f920b532.png">


# 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 システムコードを記述します。

- `0003`

<img width="909" alt="スクリーンショット 2019-12-15 0.03.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90125/f6be8d55-c108-ab50-627b-57355e3a2632.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">
[前回の記事](https://qiita.com/treastrain/items/f5c518486d19ba3573d3)と全く同じです。処理は全て `ViewController.swift` に記述します。

# コーディング
ライブラリを導入するか、導入せずに直接 Core NFC を操るかの2択になります。。

## ライブラリを導入する場合
[前回の記事](https://qiita.com/treastrain/items/f5c518486d19ba3573d3)でも登場した、私が開発・公開しているライブラリ、[treastrain/TRETJapanNFCReader](https://github.com/sakiwei/TRETJapanNFCReader) は交通系ICの残高の取得にも使用できます。導入方法は README をご覧いただくとして、導入後のサンプルコードを記載します。

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

    var reader: TransitICReader!

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

    func feliCaReaderSession(didRead feliCaCard: FeliCaCard) {
        let transitICCard = feliCaCard as! TransitICCard
        let balance = transitICCard.data.balance! // カード残高
    }
}
```

## 直接 Core NFC を使う場合
こちらも[前回の記事](https://qiita.com/treastrain/items/f5c518486d19ba3573d3)とコードの内容はほぼ同一ですので、コードの各部分の説明は前回の記事を参考にしてください。異なる部分のみ解説します。

```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 serviceCode = Data([0x00, 0x8B].reversed())
            
            /// ブロック数
            let blocks = 1
            let blockList = (0..<blocks).map { (block) -> Data in
                Data([0x80, UInt8(block)])
            }
            
            feliCaTag.readWithoutEncryption(serviceCodeList: [serviceCode], 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(11, 12)
                
                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
    }
}
```

## コード解説
### 前回の記事を参照
⏩ [対応端末かどうかを確かめる](https://qiita.com/treastrain/items/f5c518486d19ba3573d3#対応端末かどうかを確かめる)
⏩ [`NFCTagReaderSession` をスタートさせる](https://qiita.com/treastrain/items/f5c518486d19ba3573d3#nfctagreadersession-をスタートさせる)
⏩ [`NFCTagReaderSessionDelegate`](https://qiita.com/treastrain/items/f5c518486d19ba3573d3#nfctagreadersessiondelegate)
⏩ [タグに接続する](https://qiita.com/treastrain/items/f5c518486d19ba3573d3#タグに接続する)
⏩ [Read Without Encryption コマンドを送信](https://qiita.com/treastrain/items/f5c518486d19ba3573d3#read-without-encryption-コマンドを送信)

### FeliCa サービスコードの指定
```swift
/// FeliCa サービスコード
let serviceCode = Data([0x00, 0x8B].reversed())

/// ブロック数
let blocks = 1
let blockList = (0..<blocks).map { (block) -> Data in
    Data([0x80, UInt8(block)])
}
```

交通系ICカードの残高情報は FeliCa サービスコード `0x008B` に1ブロックで記録されています。リトルエンディアンで送信するために `.reversed()` しています。

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

print(data as NSData)
print("残高: ¥\(balance)")
session.alertMessage = "残高: ¥\(balance)"
session.invalidate()
```
Read Without Encryption コマンドの結果として返ってきた `blockData` の要素数は1です。
その唯一の要素である `data` の11番目と12番目の2ビットにリトルエンディアンで残高が記録されています。
NFC の通信処理をすべて終えたら `session.invalidate()` で終了します。

# 後記
内容としては楽天Edy、nanaco、WAON の読み取りとほぼ同一で、指定する FeliCa システムコード、FeliCa サービスコードが異なるというものでした。
都市部であればどうしても交通系ICが生活の一部になっているかと思いますので、あえて別記事として執筆しました。
自分のいつも使う交通系ICと iPhone でちょっと遊んでみませんか…？