LoginSignup
5
7

More than 3 years have passed since last update.

iOS13 CoreNFC NFC type-Bの読み取り

Posted at

やったこと

特定の社員証に含まれるNFCタグ情報を読み取りたい、という要望があったので試した

環境

Xcode 11 beta 6
iOS 13 beta 8
iPhoneXS Max

設定

info.plistに次を追加

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に以下が設定されていること

project.entitlements
    <key>com.apple.developer.nfc.readersession.formats</key>
    <array>
        <string>NDEF</string>
        <string>TAG</string>
    </array>

実装

ViewController
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 以降)
スマートカード / コマンドとレスポンス

5
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
7