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 version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.developer.nfc.readersession.formats</key>
  <array>
    <string>TAG</string>
  </array>
</dict>

设置 Info.plist

增加 NFC 相关的隐私设置,向 Info.plist 文件中增加 Privacy – NFC Scan Usage Description 隐私设置项。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NFCReaderUsageDescription</key>
  <string>应用需求您的同意,才干访问 NFC 进行社保卡信息的读写。</string>
</dict>

增加 AID 相关的设置项,向 Info.plist 文件中增加 ISO7816 application identifiers for NFC Tag Reader Session 装备项。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/
DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
  <array>
    <string>A000000632010105</string>
  </array>
</dict>

阐明:第三代社保卡运用一致的交通联合卡电子钱包规范,A000000632010105 为交通联合卡 AID 标识。参考网址:wiki.nfc.im/books 。

导入 CryptoSwift 第三方库

在项目导航器中选中项目,右键菜单挑选 Add Packages…,在搜索框中输入 github.com/krzyzanowsk… 并点击 Add Package 按钮完成导入。

add-package.png

阐明:CryptoSwift 提供了相关的十六进制字符串与 UInt8 相互转化的办法。

代码编程

扩展 NFCISO7816Tag

因为 Apple 是从 iOS 14 体系开端提供了 sendCommand API 的异步调用方式,为兼容 iOS 13 体系,并更好的运用 Swift 提供的 async/await 语法,现对其 NFCISO7816Tag 进行办法扩展。

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

import CoreNFC
@available(iOS 13.0, *)
class NFCISO7816TagSession: NSObject, NFCTagReaderSessionDelegate {
  private var session: NFCTagReaderSession? = nil
  private var sessionContinuation: CheckedContinuation<NFCISO7816Tag, Error>? = 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 编写如下代码所示的页面,包括一个显现卡号的标签和一个读取按钮。

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()
  }
}

完成读取逻辑

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 是卡与读卡器之间传送的信息单元,具体指令描述请参考 wiki.nfc.im/books 。

运行进程截图

screenshot.png