はじめに
こんにちは、新卒3年目エンジニアの@rs_tukkiです。先日開発オフィスが移転となり、美味しい昼食を探し求める日々が続いています。
さて、来る9/20(現地時間)、ついにiOS13がリリースされました!
ダークモードやSwiftUIなど、利用者としてだけでなく開発者としても魅力的な機能が様々追加されているそうですが、今回はその中でも、ついにアップデートされたCoreNFCについてまとめてみます。
CoreNFCとは
CoreNFCとはNFC規格の通信フレームワークのことで、appleではiOS11から採用されていました。
NFCは近距離無線通信の略で、よく皆さんが使っているSuicaやEdyなどの電子マネーや、最近ではマイナンバーカードなどにも搭載されている「かざして通信する」ことができるシステムを総称してこう呼びます。
iOS13からは、このCoreNFCの機能がアップデートされ、上記のようなカードの読み書きが、サードパーティ製のアプリでも行えるようになりました。
今までは
iOS12までのCoreNFCは、NDFC(NFC Data Exchange Format)という規格の「読み取り」にしか対応していませんでした。
NDFCはURLなど超小容量のデータしか通信できないため、CoreNFCと言いながらもほぼQRコードと同等以下のシステムでしかないと言えます。
iOS12まででSuicaカードなどの読み取りを行いたい場合、iPhoneと外部機器をBlueToothで接続する必要がありました(当然、書き込みはできません)。
楽楽精算のICリーダーも、iOS版はこの仕組みで読み取っています。
apps.apple.comSuicaの履歴を読み取ってみる
というわけで早速、アップデートされたCoreNFCの機能を使いSuicaの乗降履歴情報を取り込んでみましょう。
実装については以下のサイトを参考にさせていただきました。
Xcodeプロジェクトの設定
まずは、Xcodeのプロジェクト上でCoreNFCを使用するための設定を行います。
プロジェクトを開き、TARGETS>Signing & Capabilitiesから左上の「+」ボタンを押すと、プロジェクトに追加するネイティブ機能を選択できます。
iOS13に対応したXcodeであれば、Near Field Communication Tag Reading(=NFCタグの読み取り)の機能があるので、この機能を有効にします。
続いて、Info.plistを編集します。
ISO18092 system codes for NFC Tag Reader Sessionには、読み取りたいカードの「システムコード*1」を入力します。
たとえば、SuicaやPASMOなどの交通系ICカードを読み取りたい場合、システムコードは0003になります。
配列型で複数のシステムコードを登録できますが、ワイルドカードで全てのカードを読み取れるようにする...といったことは残念ながらできません。
読み取り開始まで
さて、前準備も終わったところで早速カードの読み取りをしてみましょう。
まず初めに、CoreNFCをインポートしておきます。
import CoreNFC
カードを読み取らせるときは、NFCTagReaderSession型の変数を定義しておき、どのような規格のカードを読み取るか設定します。iso18092はNFC TypeF、つまりSuicaに代表されるFelica規格のことを指します。 bigin()を呼び出すとカードの読み取り状態になります。現在のところはiOS固有のUIが表示されるため、独自の読み取り画面を表示させるといったことはできなくなっています。
//NFCタグを読み取るための変数を定義 var session: NFCTagReaderSession? : (中略) : //読み取り開始時 self.session = NFCTagReaderSession(pollingOption: .iso18092, delegate: self) self.session?.alertMessage = "カードをiPhoneに近付けてください。" self.session?.begin()
カードを読み取る際は、それぞれ以下に記載したタイミングで3つのメソッドが呼ばれます。
もし読み取りが何らかの理由で失敗した場合は、2番目のメソッド内でerror引数を元にエラーハンドリングを行います。
//読み取り状態になったとき func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) { } //読み取りが完了したとき func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { } //読み取りが成功したとき func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
読み取った内容の解析
続いて、読み取りに成功してからの処理です。
引数のtagsには、読み取った全てのNFC対応カードの情報が入っています。最初に認識したカードが正しく読み取れているか、Felica規格のものかどうか確認しておきましょう。
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { let tag = tags.first! session.connect(to: tag) { (error) in if nil != error { print("Error: ", error) return } guard case .feliCa(let feliCaTag) = tag else { session.alertMessage = "FeliCa以外の規格が検出されました。FeliCa規格のカードで再試行してください。" return }
このタイミングで、既にIDm*2と、先ほど指定したシステムコードは読み取ることが出来ます。
更に乗車履歴などの詳細な情報を読み取る場合、その情報が格納されている場所をサービスコードとして指定します。このとき難儀なのが、サービスコードをリトルエンディアン*3で指定しなければいけない、ということです。有志の方のサンプルコードを読む前に私も実装にチャレンジしてみたのですが、ここの指定が上手くいかず挫折していました...
その後は読み取る範囲を指定してから、requestServiceでいよいよ乗車履歴を読み出します。
中身のデータが存在していればFFが返ってくるはずですので、そこも一度確認しておきましょう。
let idm = feliCaTag.currentIDm.map { String(format: "%.2hhx", $0) }.joined() let systemCode = feliCaTag.currentSystemCode.map { String(format: "%.2hhx", $0) }.joined() let serviceCode = Data([0x09,0x0f].reversed()) feliCaTag.requestService(nodeCodeList: [serviceCode]) { nodes, error in if let error = error { print("Error:", error) return } guard let data = nodes.first, data != Data([0xff, 0xff]) else { print("履歴情報が存在しません。") return }
Suicaをはじめとする交通系ICカードは全部で20件の乗降履歴を保持できますが、現在の使用では一度に12件までしか読み取れないようです。
読み取る範囲を指定したらreadWithoutEncryptionを実行します。戻り値としてstatusが2種類返ってきますが、この値が両方とも00であれば、めでたく履歴を取得できています。
読み取り中のUIはsession.invalidate()で閉じておきましょう。
中身のデータがどのような形式で格納されているか...についてですが、独自に解析している方がおり、それを参考にすることができます。
今回はサンプルコードから引用させていただきましたが、参考資料にはデータの格納情報が更に詳しく載っていますので、気になる方は読んでみてください。
let block:[Data] = (0..<12).map { Data([0x80, UInt8($0)]) } feliCaTag.readWithoutEncryption(serviceCodeList: [serviceCode], blockList: block) {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() print("IDm: \(idm)") print("System Code: \(systemCode)") 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) } } } } }
結果確認
さて、ここまでの実装を踏まえて実際にSuicaを読み取ってみると...
が取得できているのが分かるかと思います!
まとめ
今回は、iOS13でアップデートされたCoreNFCについて説明しました。
Androidではかなり前から読み取れていましたが、ようやくiOSも追いついた形になります。これを機に今までのアプリも色々アップデートされていきそうで楽しみです!
参考
iOS13 CoreNFCの使いみちとQRコード、BLEとの比較 - Qiita
iOS13ではNFC機能をサードパーティに開放、行政手続きなどに利用可能 - iPhone Mania
「iOS 13」ベータ版をインストールする方法
iOS 13 で FeliCa (Suica) にアクセス | notes from E
iOSでSuicaの履歴を読み取る - Qiita
サイバネ規格 (ICカード) ‐ 通信用語の基礎知識
*1:カードの利用目的ごとに割り振られた番号
*2:カードごとに割り振られた番号
*3:http://e-words.jp/w/%E3%83%AA%E3%83%88%E3%83%AB%E3%82%A8%E3%83%B3%E3%83%87%E3%82%A3%E3%82%A2%E3%83%B3.html
*4:0はJR、もしくは関東の私鉄、バスを指す。