やったこと
特定の社員証に含まれるNFCタグ情報を読み取りたい、という要望があったので試した
環境
Xcode 11 beta 6
iOS 13 beta 8
iPhoneXS Max
設定
info.plistに次を追加
<key>NFCReaderUsageDescription</key>
<string>NFCタグを読み取ります</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>nfc</string>
<string>armv7</string>
</array>
<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
<array>
<string>[読み取りたいAID]</string>
</array>
info
targetの[info]にも同様の値が設定されていることを確認
(Beta 7で、なぜか同期されないことがあった)
Capabilities
[Capabilities] > [Near Field Communication Tag Reading] を追加
project.entitlementsに以下が設定されていること
<key>com.apple.developer.nfc.readersession.formats</key>
<array>
<string>NDEF</string>
<string>TAG</string>
</array>
実装
import UIKit
import CoreNFC
import Foundation
class ViewController: UIViewController {
var session: NFCTagReaderSession?
let verify: [UInt8] = [0x00, 0x00, 0x00, 0x00]// 必要な値に変える
let readBinary: [UInt8] = [0x00, 0x00, 0x00, 0x00]// 必要な値に変える
override func viewDidLoad() {
super.viewDidLoad()
}
@IBAction func scanStart(_ sender: Any) {
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
}
DispatchQueue.main.async {
self.startSession()
}
}
}
extension ViewController: NFCTagReaderSessionDelegate {
func startSession() {
self.session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)
self.session?.alertMessage = "NFCタグにiPhoneをかざしてください"
self.session?.begin()
}
// タグ取り込みウィンドウ表示時に呼ばれている
func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
print(#function)
}
// リーダーセッションを無効化した時に呼ばれる
// エラー時も意図的に閉じた時も呼び出される
func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
print(#function)
if let readerError = error as? NFCReaderError {
print("code:", readerError.code)
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))
self.present(alertController, animated: true, completion: nil)
}
}
print("sessionを破棄しました")
self.session = nil
}
// タグ検出
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
print(#function, "tag.count:", tags.count)
if tags.count > 1 {
let retryInterval = DispatchTimeInterval.milliseconds(500)
session.alertMessage = "More than 1 tag is detected, please remove all tags and try again."
DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval, execute: {
session.restartPolling()
})
return
}
let tag = tags.first!
session.connect(to: tag) { (error) in
if nil != error {
session.invalidate(errorMessage: "カードと接続できませんでした。再度読み取りしてください。")
return
}
guard case .iso7816(let typeBTag) = tag else {
let retryInterval = DispatchTimeInterval.milliseconds(500)
session.alertMessage = "NFC type-Bタグではありません"
DispatchQueue.global().asyncAfter(deadline: .now() + retryInterval, execute: {
session.restartPolling()
})
return
}
// Verify
let sendData = Data.init(self.verify)
let myAPDU = NFCISO7816APDU.init(data: sendData)!
typeBTag.sendCommand(apdu: myAPDU) { (response: Data, sw1: UInt8, sw2: UInt8, error: Error?) in
print("sw1=\(sw1)", "sw2=\(sw2)")
if let error = error {
debugPrint(error)
print("Application failure / Verify")
session.invalidate(errorMessage: "Application failure / Verify")
return
}
if !(sw1 == 0x90 && sw2 == 0) {
print("Application failure / Verify")
session.invalidate(errorMessage: "Application failure / Verify : sw1=\(sw1), sw2=\(sw2)")
return
}
// READ BINARY
let sendDataReadBinary = Data.init(self.readBinary)
let myAPDU2 = NFCISO7816APDU.init(data: sendDataReadBinary)!
typeBTag.sendCommand(apdu: myAPDU2) { (response: Data, sw1: UInt8, sw2: UInt8, error: Error?) in
print("sw1=\(sw1)", "sw2=\(sw2)")
if let error = error {
debugPrint(error)
print("Application failure / READ BINARY")
session.invalidate(errorMessage: "Application failure / READ BINARY")
return
}
if !(sw1 == 0x90 && sw2 == 0) {
print("Application failure / READ BINARY")
session.invalidate(errorMessage: "Application failure / READ BINARY : sw1=\(sw1), sw2=\(sw2)")
return
}
let resString = String(data: response, encoding: .ascii)!
print(resString)
session.alertMessage = "読み取りできました!\(resString)"
// リーダーセッションを閉じる。再利用は不可
session.invalidate()
}
}
}
}
}
補足
・iOS13 beta3まではNFCISO7816APDU.initでdataのみを指定するとnilになってしまって使えなかった(読み取れた人たちはいたので、読み取っているタグの内容によったのかも?)
・iOS13 beta7まではエラーが出てうまく取得できなかった
・AIDは16byteでなければ認識できないようだ(16byte未満は認識しなかった)
・AIDを複数検知した挙動は未確認
参考
Core Bluetooth Programming Guide
NFCPassportReader for iOS 13
#53 First steps with NFC on iOS 13
【WWDC19】Core NFC で FeliCa(Suica) を読み取るサンプル【iOS 13 以降】
iOSでSuicaの履歴を読み取る
日本の NFC、FeliCa カード向けリーダーライブラリ(iOS 13.0 以降)
スマートカード / コマンドとレスポンス