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 。