iOS 使用 CoreNFC 讀取第三代社保卡信息
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 功能。
```xml
設置 Info.plist
添加 NFC 相關的隱私設置,向 Info.plist 文件中添加 Privacy - NFC Scan Usage Description 隱私設置項。
```xml
添加 AID 相關的設置項,向 Info.plist 文件中添加 ISO7816 application identifiers for NFC Tag Reader Session 配置項。
```xml
説明:第三代社保卡使用統一的交通聯合卡電子錢包規範,A000000632010105 為交通聯合卡 AID 標識。參考網址:http://wiki.nfc.im/books 。
導入 CryptoSwift 第三方庫
在項目導航器中選中項目,右鍵菜單選擇 Add Packages...,在搜索框中輸入 http://github.com/krzyzanowskim/CryptoSwift 並點擊 Add Package 按鈕完成導入。
説明: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
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 。