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 標識。參考網址:https://wiki.nfc.im/books 。
匯入 CryptoSwift 第三方庫
在專案導航器中選中專案,右鍵選單選擇 Add Packages...,在搜尋框中輸入 https://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 是卡與讀卡器之間傳送的資訊單元,具體指令描述請參考 https://wiki.nfc.im/books 。