iOS13 CoreNFC NFC type-Bの読み取り

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

## 環境
Xcode 11 beta 6
iOS 13 beta 8
iPhoneXS Max

## 設定
### info.plistに次を追加

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

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

## 実装

```swift: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](https://developer.apple.com/library/archive/documentation/NetworkingInternetWeb/Conceptual/CoreBluetooth_concepts/AboutCoreBluetooth/Introduction.html)
[NFCPassportReader for iOS 13](https://github.com/AndyQ/NFCPassportReader)
[#53 First steps with NFC on iOS 13](https://swifting.io/blog/2019/07/11/53-first-steps-with-nfc-on-ios-13/)
[【WWDC19】Core NFC で FeliCa(Suica) を読み取るサンプル【iOS 13 以降】](https://qiita.com/treastrain/items/23d343d2c215ab53ecbf)
[iOSでSuicaの履歴を読み取る](https://qiita.com/m__ike_/items/7dc3e643396cf3381167)
[日本の NFC、FeliCa カード向けリーダーライブラリ（iOS 13.0 以降）](https://github.com/treastrain/TRETJapanNFCReader)
[スマートカード / コマンドとレスポンス](http://eternalwindows.jp/security/scard/scard07.html)