運転免許証、物販向け電子マネー(楽天Edy、nanaco、WAON)に引き続き、今回は交通系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)
実行結果
Xcode プロジェクトの作成
Create a new Xcode project から iOS の Single View App を作成します。
Capability と Entitlements の設定
Project の Target から Signing & Capabilities を選択し、「+ Capability」から「Near Field Communication Tag Reading」を追加します。
すると、「<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
ファイルツリー
今回のプロジェクトのファイルツリーを確認しておきます。
前回の記事と全く同じです。処理は全て ViewController.swift
に記述します。
コーディング
ライブラリを導入するか、導入せずに直接 Core NFC を操るかの2択になります。。
ライブラリを導入する場合
前回の記事でも登場した、私が開発・公開しているライブラリ、treastrain/TRETJapanNFCReader は交通系ICの残高の取得にも使用できます。導入方法は README をご覧いただくとして、導入後のサンプルコードを記載します。
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 を使う場合
こちらも前回の記事とコードの内容はほぼ同一ですので、コードの各部分の説明は前回の記事を参考にしてください。異なる部分のみ解説します。
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
}
}
コード解説
前回の記事を参照
⏩ 対応端末かどうかを確かめる
⏩ NFCTagReaderSession
をスタートさせる
⏩ NFCTagReaderSessionDelegate
⏩ タグに接続する
⏩ Read Without Encryption コマンドを送信
FeliCa サービスコードの指定
/// 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()
しています。
データの解析
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 でちょっと遊んでみませんか…?