What's new in Core NFC
System & Services 进阶 22m

Core NFC 新特性概览

What's new in Core NFC

2020年6月25日

在 Apple 官方观看视频

一句话判断

Core NFC 在 iOS 14 终于支持了 NDEF 写入和 tag 系统级检测——如果你的 NFC 应用还停留在”只读”阶段,是时候升级了。

这场 Session 讲了什么

Core NFC 框架在 iOS 14 中获得了多项重要更新。最显著的变化是支持了 NDEF(NFC Data Exchange Format)消息的写入能力——此前 Core NFC 只能读取 NDEF 标签,现在可以向空白 NDEF 标签写入 URL、文本、联系人等数据。

Session 详细介绍了新的 tag 类型支持。Core NFC 现在可以与更多类型的 NFC 标签交互,包括 ISO 7816、ISO 15693、FeliCa 和 MIFARE 标签的读写操作。这些标签广泛用于门禁卡、公交卡、电子名片和产品防伪等场景。

另一个重要更新是 NFC 标签的系统级检测。iOS 14 中,当 iPhone 检测到背景标签(background tag)时,系统会自动弹出通知,用户点击后直接打开你的应用并传递标签数据。在此之前,用户必须先打开你的应用,然后再贴标签——这个流程反直觉。新流程更自然:先贴标签,系统自动识别并打开对应应用。

Session 还介绍了 NFCNDEFReaderSessionNFCTagReaderSession 的 API 改进,包括更灵活的会话配置和错误处理。

值得深挖的点

Background Tag Reading 的用户体验

Background tag reading 在 iPhone XS 及以上机型上可用。当手机靠近一个包含 NDEF 消息的 NFC 标签时,系统会推送一个通知横幅。用户点击横幅后,系统会根据 NDEF 消息中的 URL scheme 或 Universal Link 打开对应的应用。这对 App Clips 特别有价值——用户把手机贴近餐厅桌上的 NFC 标签,系统自动弹出 App Clip 通知。

NDEF 写入的实际应用

NDEF 写入让 iPhone 变成了一个 NFC 标签编程器。你可以将 URL、Wi-Fi 配置、联系人信息(vCard)等数据写入空白 NDEF 标签。一个典型的应用场景是”一键配网”——用户把新买的 IoT 设备靠近 iPhone,你的应用把 Wi-Fi 凭证写入设备的 NFC 标签,设备读取后自动连接网络。

代码片段

import CoreNFC

// NDEF 标签读写
class NFCManager: NSObject, NFCNDEFReaderSessionDelegate {
    var session: NFCNDEFReaderSession?
    
    // 扫描并读取 NDEF 标签
    func startReading() {
        guard NFCNDEFReaderSession.readingAvailable else {
            print("此设备不支持 NFC 读取")
            return
        }
        
        session = NFCNDEFReaderSession(
            delegate: self,
            queue: nil,
            invalidateAfterFirstRead: false  // 持续读取多个标签
        )
        session?.alertMessage = "将 iPhone 靠近 NFC 标签"
        session?.begin()
    }
    
    // 读取到 NDEF 消息
    func readerSession(_ session: NFCNDEFReaderSession,
                       didDetectNDEFs messages: [NFCNDEFMessage]) {
        for message in messages {
            for record in message.records {
                switch record.typeNameFormat {
                case .absoluteURI:
                    let url = String(data: record.payload, encoding: .utf8)
                    print("检测到 URL: \(url ?? "")")
                case .nfcWellKnown:
                    if let text = parseTextRecord(record) {
                        print("检测到文本: \(text)")
                    }
                default:
                    break
                }
            }
        }
    }
    
    func readerSession(_ session: NFCNDEFReaderSession,
                       didInvalidateWithError error: Error) {
        print("NFC 会话结束: \(error.localizedDescription)")
    }
    
    func parseTextRecord(_ record: NFCNDEFPayload) -> String? {
        guard record.typeNameFormat == .nfcWellKnown,
              record.type == "T".data(using: .ascii) else { return nil }
        
        let payload = record.payload
        guard payload.count > 3 else { return nil }
        
        let statusByte = payload[0]
        let languageLength = Int(payload[1])
        let textData = payload.subdata(in: (2 + languageLength)..<payload.count)
        return String(data: textData, encoding: .utf8)
    }
}
// 向 NFC 标签写入 NDEF 数据
extension NFCManager {
    
    func readerSession(_ session: NFCNDEFReaderSession,
                       didDetect tags: [NFCNDEFTag]) {
        guard let tag = tags.first else { return }
        
        // 连接到标签
        session.connect(to: tag) { error in
            if let error = error {
                session.alertMessage = "连接失败: \(error.localizedDescription)"
                session.invalidate()
                return
            }
            
            // 创建要写入的 NDEF 消息
            let urlPayload = NFCNDEFPayload.wellKnownTypeURIPayload(
                string: "https://example.com/appclip?store=123"
            )!
            
            let message = NFCNDEFMessage(records: [urlPayload])
            
            // 写入标签
            tag.writeNDEF(message) { error in
                if let error = error {
                    session.alertMessage = "写入失败: \(error.localizedDescription)"
                } else {
                    session.alertMessage = "写入成功!"
                }
                session.invalidate()
            }
        }
    }
    
    // 写入文本类型的 NDEF 记录
    func writeTextRecord(text: String, to tag: NFCNDEFTag, session: NFCNDEFReaderSession) {
        let textPayload = NFCNDEFPayload.wellKnownTypeTextPayload(
            string: text,
            locale: Locale(identifier: "zh-CN")
        )
        
        let message = NFCNDEFMessage(records: [textPayload!])
        
        tag.writeNDEF(message) { error in
            if let error = error {
                session.alertMessage = "写入失败: \(error.localizedDescription)"
            } else {
                session.alertMessage = "文本写入成功"
            }
            session.invalidate()
        }
    }
}
// ISO 7816 标签的高级读写
extension NFCManager {
    
    func readISO7816Tag() {
        guard NFCReaderSession.readingAvailable else { return }
        
        let session = NFCTagReaderSession(
            pollingOption: .iso14443,
            delegate: self,
            queue: nil
        )
        session?.alertMessage = "将 iPhone 靠近智能卡"
        session?.begin()
    }
}

extension NFCManager: NFCTagReaderSessionDelegate {
    func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {
        // 会话已激活
    }
    
    func tagReaderSession(_ session: NFCTagReaderSession,
                          didInvalidateWithError error: Error) {
        print("Tag reader 会话结束: \(error.localizedDescription)")
    }
    
    func tagReaderSession(_ session: NFCTagReaderSession,
                          didDetect tags: [NFCTag]) {
        guard let tag = tags.first else { return }
        
        switch tag {
        case .iso7816(let isoTag):
            session.connect(to: isoTag) { error in
                guard error == nil else { return }
                
                // 发送 APDU 命令
                let apdu = NFCISO7816APDU(
                    instructionClass: 0x00,
                    instructionCode: 0xA4,
                    p1Parameter: 0x04,
                    p2Parameter: 0x00,
                    data: Data([0x01, 0x02]),
                    expectedResponseLength: -1
                )
                
                isoTag.sendCommand(apdu: apdu) { data, sw1, sw2, error in
                    if error == nil {
                        print("APDU 响应: SW1=\(sw1), SW2=\(sw2)")
                        print("数据: \(data.map { String(format: "%02x", $0) }.joined())")
                    }
                    session.alertMessage = "读取完成"
                    session.invalidate()
                }
            }
        default:
            break
        }
    }
}

最佳实践

  • Info.plist 中添加 NFCReaderUsageDescription,用一句话解释为什么需要 NFC 权限
  • 使用 invalidateAfterFirstRead = false 来持续扫描多个标签,适合批量写入场景
  • Background tag reading 只支持 NDEF 格式,其他格式需要用户先打开应用再扫描
  • 写入标签前先检查标签是否为只读,避免写入失败
  • 会话超时时间为 60 秒,长时间操作时更新 alertMessage 防止会话自动关闭

还有什么值得关注

  • Background tag reading 与 App Clips 配合使用,用户无需解锁手机即可触发 App Clip 体验
  • Session 提到 MIFARE 标签的支持在 iOS 14 中得到了增强,支持更多的命令操作
  • NFC 标签写入功能可以用于创建”智能名片”——将自己的联系方式写入空白 NFC 贴纸
WWDC 2020