导读:本期聚焦于越南程序员创作的《如何在iOS中通过Core NFC读写Type 4 Tag的NDEF自定义数据?》,敬请观看详情。把NFC Forum Type 4 Tag当成普通NDEF标签直接写入,常常会遇到NDEF记录容量与TLV封装不一致的问题。Type 4 Tag与大部分卡片不同,它通过ISO 7816-4 APDU访问Capability Container文件和NDEF文件,写入前必须先确认NDEF文件ID和可用容量。iOS 13之后,NFCTagReaderSession已经可以通过NFCNDEFTag协议直接读写这类标签,不需要手动拼TLV字节。本文将围绕Swift和Core NFC展开,先连接并识别Type 4 Tag,再介绍如何使用NFCNDEFPayload封装一条自定义外部类型记录,把JSON订单数据写入卡片,并给出读取与容量校验流程。针对部分标签对NDEF抽象支持不完整的情况,文章还会演示SELECT、READ BINARY和UPDATE BINARY等APDU命令作为兜底方案。读完可以掌握Type 4 Tag的读写细节,避免出现在测试卡正常、目标卡写入失败这类问题。

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 访问方式。

如何在iOS中通过Core NFC读写Type 4 Tag的NDEF自定义数据?

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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0918/58931.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。