iOS 使用 CoreNFC 讀取第三代社保卡信息

語言: CN / TW / HK

theme: cyanosis highlight: atom-one-dark


NFC 是 Near Field Communication 的縮寫,即近場通信,是一種用於短距離無線設備與其他設備共享數據或觸發這些設備上的操作的技術。它使用射頻場構建,允許沒有任何電源的設備存儲小塊數據,同時還允許其他供電設備讀取該數據。

iOS 和 watchOS 設備內置 NFC 硬件已經很多年了。在現實生活中,Apple Pay 就是使用這項技術與商店的支付終端進行交互。然而直到 iOS 11 開發者才能夠使用 NFC 硬件。後來 Apple 在 iOS 13 系統中提升了 CoreNFC 的功能,開發者可以藉助這項新技術,對 iOS 設備進行編程,使其以新的方式與周圍的互聯世界進行交互。

説明:本文提供的代碼示例所用的開發環境為 Xcode14 + Swift 5.7 + iOS 13。需要登錄已付費的開發者賬號才能開啟 NFC Capability。

工程配置

設置 Capability

在項目導航器中選中項目,轉到 Signing & Capabilities 標籤頁並選擇 +Capability,在彈出的列表中選擇 Near Field Communication Tag Reading。這會自動生成 entitlements 文件中的必要配置信息,同時為您的應用程序激活 NFC 功能。

add-capability.png

```xml

com.apple.developer.nfc.readersession.formats TAG ```

設置 Info.plist

添加 NFC 相關的隱私設置,向 Info.plist 文件中添加 Privacy - NFC Scan Usage Description 隱私設置項。

```xml

NFCReaderUsageDescription 應用需要您的同意,才能訪問 NFC 進行社保卡信息的讀寫。 ```

添加 AID 相關的設置項,向 Info.plist 文件中添加 ISO7816 application identifiers for NFC Tag Reader Session 配置項。

```xml

com.apple.developer.nfc.readersession.iso7816.select-identifiers A000000632010105 ```

説明:第三代社保卡使用統一的交通聯合卡電子錢包規範,A000000632010105 為交通聯合卡 AID 標識。參考網址:http://wiki.nfc.im/books 。

導入 CryptoSwift 第三方庫

在項目導航器中選中項目,右鍵菜單選擇 Add Packages...,在搜索框中輸入 http://github.com/krzyzanowskim/CryptoSwift 並點擊 Add Package 按鈕完成導入。

add-package.png

説明:CryptoSwift 提供了相關的十六進制字符串與 UInt8 相互轉換的方法。

代碼編程

擴展 NFCISO7816Tag

由於 Apple 是從 iOS 14 系統開始提供了 sendCommand API 的異步調用形式,為兼容 iOS 13 系統,並更好的使用 Swift 提供的 async/await 語法,現對其 NFCISO7816Tag 進行方法擴展。

```swift import CoreNFC import CryptoSwift

@available(iOS 13.0, *) extension NFCISO7816Tag {

@discardableResult   func sendCommand( command: String) async throws -> Data {     return try await withCheckedThrowingContinuation { continuation in       // 通過 CryptoSwift 庫提供的 API,將十六進制表示命令字符串轉換成字節       let apdu = NFCISO7816APDU(data: Data(hex: command))!       // 將同步調用形式轉換成異步調用形式       sendCommand(apdu: apdu) { responseData, , _, error in         if let error {           continuation.resume(throwing: error)         } else {           continuation.resume(returning: responseData)         }       }     }   } } ```

封裝 NFCTagReaderSession

```swift import CoreNFC

@available(iOS 13.0, *) class NFCISO7816TagSession: NSObject, NFCTagReaderSessionDelegate {

private var session: NFCTagReaderSession? = nil   private var sessionContinuation: CheckedContinuation? = nil

func begin() async throws -> NFCISO7816Tag { // 實例化用於檢測 NFCISO7816Tag 的會話     session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self)     session?.alertMessage = "請將社保卡靠近手機背面上方的 NFC 感應區域"     session?.begin()     return try await withCheckedThrowingContinuation { continuation in       self.sessionContinuation = continuation     }   }

func invalidate(with message: String) { // 關閉讀取會話,以防止重用     session?.alertMessage = message     session?.invalidate()   }

// MARK: - NFCTagReaderSessionDelegate

func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {}

func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { // 檢測到 NFCISO7816Tag     if let tag = tags.first, case .iso7816(let iso7816Tag) = tag {       session.alertMessage = "正在讀取信息,請勿移動社保卡" // 連接到 NFCISO7816Tag 並將同步調用形式轉換成異步調用形式       session.connect(to: tag) { error in         if let error {           self.sessionContinuation?.resume(throwing: error)         } else {           self.sessionContinuation?.resume(returning: iso7816Tag)         }       }     }   }

func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { // 讀取過程中發生錯誤     self.session = nil     sessionContinuation?.resume(throwing: error)   } } ```

編寫 UI 界面

使用 SwiftUI 編寫如下代碼所示的頁面,包含一個顯示卡號的標籤和一個讀取按鈕。

```swift import SwiftUI

struct ContentView: View {   @State private var cardNo = ""

var body: some View {     VStack(alignment: .leading) {       Text("卡號:(cardNo)")         .font(.system(size: 17))       Button(action: read) {         Text("讀取")           .padding()           .frame(maxWidth: .infinity)           .foregroundColor(.white)           .background(.blue)           .cornerRadius(8)       }       Spacer()     }     .padding()   } } ```

實現讀取邏輯

```swift import SwiftUI import CryptoSwift

struct ContentView: View { // var body: some View {...}

private func read() {     Task {       let session = NFCISO7816TagSession()       do { // 檢測 NFCISO7816Tag         let tag = try await session.begin() // 發送命令 00B0950A12 並截取前 10 個字節轉換為 20 位卡號         let cardNo = try await tag.sendCommand("00B0950A12")[0..<10].toHexString()         self.cardNo = cardNo // 關閉讀取會話         session.invalidate(with: "讀取成功")       } catch {         print(error)       }     }   } } ```

説明:APDU 是卡與讀卡器之間傳送的信息單元,具體指令描述請參考 http://wiki.nfc.im/books 。

運行過程截圖

screenshot.png