NFC Forum Type 4 Tag 与常见的 NTAG 系列(Type 2)在存储模型上有明显差异。Type 2 Tag 通常通过内存地址读写 TLV 结构,而 Type 4 Tag 走 ISO 7816-4 文件系统,NDEF 数据保存在一个由 Capability Container(CC)文件声明的 NDEF 文件里。iOS 的 Core NFC 框架从 iOS 13 开始,对 Type 4 Tag 的 NDEF 读写作了抽象,开发者可以把 NFCISO7816Tag 转换成 NFCNDEFTag,直接用 readNDEF 和 writeNDEF 方法操作。但这并不代表可以忽略底层结构,尤其是遇到自定义数据格式、容量限制或不同厂商实现差异时,仍然需要理解 NDEF 记录封装和 APDU 访问方式。

Type 4 Tag 的读写链路比普通 NFC 标签长一些:先连接标签,再通过 NDEF 文件模型写入数据,必要时还要下降到 APDU 指令。下面从连接、NDEF 封装、标准读写和 APDU 兜底四个角度拆解实现。
一、启用 Core NFC 与连接 Type 4 Tag
使用 Core NFC 之前,需要在 Xcode 工程中开启 Near Field Communication Tag Reading 能力,并在 Info.plist 中加入 NFCReaderUsageDescription 隐私描述。如果你的应用还需要在 Type 4 Tag 上执行自定义 AID 的 APDU 指令,则需要在 entitlements 中补充 ISO7816 application identifiers 列表;如果只是读写 NDEF,系统会默认选择 NFC Forum 的 NDEF AID,通常不需要额外声明。
扫描阶段推荐使用 NFCTagReaderSession 而不是 NFCNDEFReaderSession,因为 Type 4 Tag 本质是 ISO 14443-4 卡片,使用 NFCTagReaderSession 可以直接拿到 NFCISO7816Tag 对象。轮询选项选择 .iso14443 即可。连接成功后,didDetect 回调中的 tag 参数可以通过 case let .iso7816 取出。
下面是一段基本的扫描与连接代码。
import CoreNFC
class NFCReaderWriter: NSObject, NFCTagReaderSessionDelegate {
var session: NFCTagReaderSession?
func beginScan() {
guard NFCTagReaderSession.readingAvailable else {
print("设备不支持 NFC")
return
}
session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: nil)
session?.alertMessage = "请将 iPhone 靠近 NFC Forum Type 4 Tag"
session?.begin()
}
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
guard let tag = tags.first, case let .iso7816(iso7816Tag) = tag else {
session.invalidate(errorMessage: "未识别到 Type 4 Tag")
return
}
session.connect(to: tag) { error in
if let error = error {
session.invalidate(errorMessage: "连接失败: \(error.localizedDescription)")
return
}
self.handleType4Tag(iso7816Tag, session: session)
}
}
func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {
self.session = nil
}
}
这里把连接后的处理放到 handleType4Tag 中,是为了让后续读写逻辑更集中。实际工程里建议把 NFC 会话和业务解析拆开,避免扫码页面和数据处理耦合过重。
二、NDEF 记录封装与自定义数据格式
NDEF 数据不是简单字节流,它由一条或多条 NDEF Record 组成。每条 Record 包含 TNF、type、identifier 和 payload。iOS 中的 NFCNDEFPayload 和 NFCNDEFMessage 分别对应 Record 和 Message。自定义业务数据不建议使用 TNF 为 well-known 的 Text 或 URI 记录,因为通用阅读器会按照标准格式解析,容易把你的 JSON 当作可展示文本。更适合的做法是使用 TNF 为 external 的记录,把 type 写成域名反写形式,例如 ipipp.com:order。这样既避免了与标准类型冲突,又能在后端或其它读卡设备中根据 type 判断 payload 结构。
payload 部分可以选择 JSON 或二进制编码。JSON 的优势是结构清晰、调试方便,适合小容量数据;如果容量很紧张,可以改为二进制格式,但需要自行维护协议版本和字段顺序。下面示例使用 Codable 结构体生成 JSON。
import CoreNFC
struct Order: Codable {
var orderId: String
var amount: Double
var timestamp: TimeInterval
}
func buildCustomNDEFMessage(order: Order) throws -> NFCNDEFMessage {
let payloadData = try JSONEncoder().encode(order)
let typeData = Data("ipipp.com:order".utf8)
let record = NFCNDEFPayload(
format: .external,
type: typeData,
identifier: Data(),
payload: payloadData
)
return NFCNDEFMessage(records: [record])
}
这里 identifier 传空 Data 是为了省下几个字节。Type 4 Tag 的 NDEF 文件通常并不大,部分卡可能只有几百字节甚至更少,每一条 Record 的 type、identifier 和 payload 都会计入总容量。写入前一定要做容量预算,不要想当然地认为所有 Type 4 Tag 都拥有充足的存储空间。
另外,如果需要一次写入多条业务记录,可以把多个 NFCNDEFPayload 放进同一个 NFCNDEFMessage,读取端再根据 type 逐条筛选。但在 Type 4 Tag 上,NDEF 写入接口通常是一次性替换整个 NDEF 文件内容,所以多条记录必须一次构造完整,不能像追加日志一样多次追加。
三、使用 NFCNDEFTag 协议读写 Type 4 Tag
连接成功后,NFCISO7816Tag 在 iOS 13 及以上版本通常可以转换为 NFCNDEFTag。转换成功后先调用 queryNDEFStatus(completionHandler:),回调中可以得到当前 NDEF 状态和可用容量。可用容量是写入前最重要的参数,忽略它很容易触发 NFCReaderError.capacity 错误。下面的代码展示了查询容量、构造消息、校验长度、写入标签的完整流程。
func handleType4Tag(_ tag: NFCISO7816Tag, session: NFCTagReaderSession) {
guard let ndefTag = tag as? NFCNDEFTag else {
session.invalidate(errorMessage: "该 Type 4 Tag 不支持 NDEF 抽象")
return
}
ndefTag.queryNDEFStatus { status, capacity, error in
if let error = error {
session.invalidate(errorMessage: "查询 NDEF 状态失败: \(error.localizedDescription)")
return
}
let order = Order(orderId: "A1001", amount: 29.9, timestamp: Date().timeIntervalSince1970)
guard let message = try? buildCustomNDEFMessage(order: order) else {
session.invalidate(errorMessage: "NDEF 记录构造失败")
return
}
let payloadLength = message.records.reduce(0) { $0 + $1.payload.count }
guard payloadLength <= capacity else {
session.invalidate(errorMessage: "容量不足,当前可用:\(capacity) 字节")
return
}
ndefTag.writeNDEF(message) { error in
if let error = error {
session.invalidate(errorMessage: "写入失败: \(error.localizedDescription)")
} else {
session.alertMessage = "写入成功"
session.invalidate()
}
}
}
}
读取侧同样依赖 NFCNDEFTag。调用 readNDEF(completionHandler:) 后,回调会返回一个 NFCNDEFMessage,遍历 records,找到 type 为 ipipp.com:order 的记录,再用 JSONDecoder 还原为 Order 结构体。读取流程比写入简单,但同样要注意会话时间很有限,应当在拿到数据后尽快处理并 invalidate 会话,避免用户一直拿着手机贴近卡片。
写失败时不要立刻放弃。部分标签在写入失败后容量状态可能发生变化,可以重新调用 queryNDEFStatus,确认最新可用容量和 NDEF 状态。还要区分是容量问题、标签写保护,还是连接中途断开。连接断开通常无法在当前会话内恢复,需要引导用户重新扫描。
四、APDU 直接访问作为兜底方案
虽然 NFCNDEFTag 抽象能覆盖大部分 Type 4 Tag,但仍有一些厂商标签或特殊配置无法通过该协议正常读写。这时可以退回到 NFCISO7816Tag 的原生 APDU 能力。Type 4 Tag 的标准 NDEF 应用 AID 是 D2760000850101,用 SELECT 命令选中后,再读取 Capability Container 文件分析 NDEF 文件位置和容量。下面先展示 SELECT 命令的 Swift 实现。
func selectNDEFApplication(on tag: NFCISO7816Tag, completion: @escaping (Data?, Error?) -> Void) {
let selectCommand = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0xA4,
p1Parameter: 0x04,
p2Parameter: 0x00,
data: Data([0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01]),
expectedResponseLength: -1
)
tag.sendCommand(apdu: selectCommand) { responseData, sw1, sw2, error in
if let error = error {
completion(nil, error)
return
}
guard sw1 == 0x90, sw2 == 0x00 else {
completion(nil, NSError(domain: "NFC", code: Int(sw1) << 8 | Int(sw2), userInfo: [NSLocalizedDescriptionKey: "SELECT 失败"]))
return
}
completion(responseData, nil)
}
}
选中 NDEF 应用后,通常还需要用 SELECT 命令选择 Capability Container 文件,文件 ID 一般是 E103。选择成功后发送 READ BINARY 读取 CC 前若干个字节,解析其中的 NDEF File Control TLV。这个 TLV 的 tag 为 0x04,length 为 0x06,后面依次是 NDEF 文件 ID 两个字节、最大 NDEF 容量两个字节以及访问权限字节。拿到 NDEF 文件 ID 后,再选择该文件并读写其内容。
func readCapabilityContainer(on tag: NFCISO7816Tag, completion: @escaping (Data?, Error?) -> Void) {
let selectCC = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0xA4,
p1Parameter: 0x00,
p2Parameter: 0x0C,
data: Data([0xE1, 0x03]),
expectedResponseLength: -1
)
tag.sendCommand(apdu: selectCC) { _, sw1, sw2, error in
if let error = error {
completion(nil, error)
return
}
guard sw1 == 0x90, sw2 == 0x00 else {
completion(nil, NSError(domain: "NFC", code: Int(sw1) << 8 | Int(sw2), userInfo: [NSLocalizedDescriptionKey: "选择 CC 文件失败"]))
return
}
let readBinary = NFCISO7816APDU(
instructionClass: 0x00,
instructionCode: 0xB0,
p1Parameter: 0x00,
p2Parameter: 0x00,
data: Data(),
expectedResponseLength: 15
)
tag.sendCommand(apdu: readBinary) { data, sw1, sw2, error in
if let error = error {
completion(nil, error)
return
}
guard sw1 == 0x90, sw2 == 0x00 else {
completion(nil, NSError(domain: "NFC", code: Int(sw1) << 8 | Int(sw2), userInfo: [NSLocalizedDescriptionKey: "读取 CC 文件失败"]))
return
}
completion(data, nil)
}
}
}
在 APDU 直连模式下,容量大小必须从 CC 文件解析出来,不能依赖 queryNDEFStatus 返回的 capacity。Type 4 Tag 的 NDEF 文件通常带有两字节的 NLEN 前缀,表示当前 NDEF 消息长度。写入时先写入 NLEN,再写入 NDEF Message 本体;读取时先读前两字节获得长度,再按长度读取后续数据。如果 NDEF 消息超过 255 字节,需要注意长度字段是高字节在前,且 UPDATE BINARY 有时需要分块完成。
APDU 方案虽然灵活,但复杂度也明显上升。需要自己处理 SELECT 结果判断、CC 文件解析、跨命令状态维护、数据分块以及 NLEN 更新。一般只有在 NFCNDEFTag 不可用、标签厂商有特殊扩展需求,或者需要精确控制底层命令时才推荐走这条路。调试时可以先用手机系统自带的 NFC 读取或 PC 读卡器确认 Type 4 Tag 的 CC 文件内容,与代码解析结果对照,能显著减少定位问题的时间。
Type 4 Tag 的 NDEF 写入核心不是把 JSON 字符串直接塞进卡片,而是先理解文件模型和记录结构,再通过标准协议或 APDU 命令完成数据落盘。多数业务场景使用 NFCNDEFTag 协议即可,容量校验和错误处理是关键;遇到兼容性问题时,再用 APDU 命令深入底层排查。掌握这两层手段后,处理不同厂商的 Type 4 Tag 会稳定很多。
Core NFCNFC Forum Type 4 TagNDEF记录修改时间:2026-09-18 19:23:58