【iOS13】Core NFC で 交通系ICカードを読み取る

iOS13で、CoreNFC を使って、交通系ICカードの「ICOCA」を読み取ってみました。

    import UIKit
    import CoreNFC

    class ViewController: UIViewController, NFCTagReaderSessionDelegate {

        var session: NFCTagReaderSession?

        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
        }

        @IBAction func beginScanning(_ sender: UIButton) {
            guard NFCTagReaderSession.readingAvailable else {
                let alertController = UIAlertController(
                    title: "Scanning Not Supported",
                    message: "This device doesn't support tag scanning.",
                    preferredStyle: .alert
                )
                alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                self.present(alertController, animated: true, completion: nil)
                return
            }

            self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self)
            self.session?.alertMessage = "Hold your iPhone near the item to learn more about it."
            self.session?.begin()
        }

        func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
            print("tagReaderSessionDidBecomeActive(_:)")
        }

        func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
            if let readerError = error as? NFCReaderError {
                if (readerError.code != .readerSessionInvalidationErrorFirstNDEFTagRead)
                    && (readerError.code != .readerSessionInvalidationErrorUserCanceled) {
                    let alertController = UIAlertController(
                        title: "Session Invalidated",
                        message: error.localizedDescription,
                        preferredStyle: .alert
                    )
                    alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
                    DispatchQueue.main.async {
                        self.present(alertController, animated: true, completion: nil)
                    }
                }
            }

            self.session = nil
        }

        func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
            guard let tag = tags.first, case let .feliCa(felicaTag) = tag else { return }    // ①

            session.connect(to: tag) { error in    // ②
                if let error = error {
                    print("Error: ", error)
                    return
                }

                let historyServiceCode = Data([0x09, 0x0f].reversed())    // ③
                felicaTag.requestService(nodeCodeList: [historyServiceCode]) { nodes, error in    // ④
                    if let error = error {
                        print("Error: ", error)
                        return
                    }

                    guard let data = nodes.first, data != Data([0xff, 0xff]) else {    // ⑤
                        print("サービスが存在しない")
                        return
                    }

                    let blockList = (0..<12).map { Data([0x80, UInt8($0)]) }    // ⑥
                    felicaTag.readWithoutEncryption(serviceCodeList: [historyServiceCode], blockList: blockList)    // ⑦
                    { status1, status2, dataList, error in
                        if let error = error {
                            print("Error: ", error)
                            return
                        }
                        guard status1 == 0x00, status2 == 0x00 else {    // ⑧
                            print("ステータスフラグエラー: ", status1, " / ", status2)
                            return
                        }
                        session.invalidate()    // ⑨

                        // ここでデータ(dataList)の解析処理

                        dataList.forEach { data in
                            print("年: ", Int(data[4] >> 1) + 2000)
                            print("月: ", ((data[4] & 1) == 1 ? 8 : 0) + Int(data[5] >> 5))
                            print("日: ", Int(data[5] & 0x1f))
                            print("入場駅コード: ", data[6...7].map { String(format: "%02x", $0) }.joined())
                            print("出場駅コード: ", data[8...9].map { String(format: "%02x", $0) }.joined())
                            print("入場地域コード: ", String(Int(data[15] >> 6), radix: 16))
                            print("出場地域コード: ", String(Int((data[15] & 0x30) >> 4), radix: 16))
                            print("残高: ", Int(data[10]) + Int(data[11]) << 8)
                        }

                    }
                }
            }
        }

    }
年:  2012
月:  7
日:  7
入場駅コード:  ???? (←16進数の数値が入ります)
出場駅コード:  ????(←16進数の数値が入ります)
入場地域コード:  0
出場地域コード:  0
残高:  20
・・・

【参考サイト】

> iOSでSuicaの履歴を読み取る - Qiita

>【WWDC19】Core NFC で FeliCa(Suica) を読み取るサンプル【iOS 13 以降】 - Qiita