在冷链监控、疫苗运输和食品仓储场景中,NFC温度记录仪凭借无源和低功耗特性被广泛采用。这类设备通常把一段时间内的温度采样结果写入NDEF记录,iOS应用通过Core NFC的读取会话拿到原始字节后,需要完成记录定位、负载解析、时间推算以及可视化展示。下面从工程实现角度拆解整套链路。

NDEF记录结构与iOS端读取入口
NDEF是NFC论坛定义的数据交换格式,一个NFCNDEFMessage由一个或多个NFCNDEFRecord组成。温度记录仪常见的做法是将数据放在MIME类型记录或者外部类型记录中,负载开头往往带有少量元数据。iOS的Core NFC在iOS 11之后提供NFCTagReaderSession,当扫描到ISO 15693或NTAG系列标签时,可以通过queryNDEFStatus拿到消息对象。
在代码层面,首先需要确认会话代理正确实现,并在获取到NFCNDEFMessage后遍历records数组。每个record的type和payload属性都是Data类型,开发者必须根据设备协议文档判断哪一条记录承载温度数据。很多记录使用RTD_TEXT时,第一个字节的高位表示编码与语言码长度,真正内容从偏移之后开始,忽略这一步会导致解析出乱码。
以下示例展示如何开启会话并安全提取第一条看起来像温度数据的记录。注意真机需要配置NFC权限描述,并且在Info.plist里声明com.apple.developer.nfc.readersession.formats。
import CoreNFC
class TempReader: NSObject, NFCTagReaderSessionDelegate {
var session: NFCTagReaderSession?
func start() {
session = NFCTagReaderSession(pollingOption: .iso15693, delegate: self)
session?.alertMessage = "请将手机靠近温度记录仪"
session?.begin()
}
func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) {
guard let tag = tags.first else { return }
session.connect(to: tag) { error in
if error != nil { session.invalidate(errorMessage: "连接失败"); return }
if case let .iso15693(isoTag) = tag {
isoTag.readNDEF { message, err in
if let msg = message {
self.parse(message: msg)
}
session.invalidate()
}
}
}
}
func parse(message: NFCNDEFMessage) {
for record in message.records {
let typeStr = String(data: record.type, encoding: .utf8) ?? ""
if typeStr.contains("temp") || record.type.count == 0 {
let payload = record.payload
print("找到疑似温度记录,长度(payload.count)")
}
}
}
func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) {}
func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) {}
}
温度负载解析与时间戳重建
拿到payload之后,核心难点在于理解厂商自定义的二进制布局。典型方案是用前两个字节表示采样间隔秒数,随后每两个字节为一个有符号小端整数,放大十倍代表摄氏温度。例如数值258表示25.8度。如果设备采用大端序,直接使用Data的withUnsafeBytes按UInt16读取会得到错误结果,必须明确使用littleEndian属性转换。
时间戳重建依赖记录写入时的起始时间。部分记录仪在NDEF文本记录里用ISO8601字符串给出开始时刻,也有设备把起始UNIX时间塞进负载头四个字节。工程上建议优先读取专用头,若缺失则退化为使用手机读取时刻减去总时长来估算。下面的函数演示如何从一个假设布局中提取温度数组和对应的时间点。
解析过程应当做边界保护,当payload长度不是预期倍数时直接丢弃,避免后续绘制越界。同时温度值如果超出合理物理范围,比如低于零下五十或高于零上一百,可以视为坏点予以剔除,保证曲线可信度。
struct Sample {
let time: Date
let temp: Double
}
func decode(payload: Data, start: Date, interval: Int) -> [Sample] {
guard payload.count >= 2 else { return [] }
let intervalSec = Int(payload[0]) | (Int(payload[1]) << 8)
var result: [Sample] = []
var offset = 2
var index = 0
while offset + 1 < payload.count {
let raw = Int(payload[offset]) | (Int(payload[offset+1]) << 8)
let signed = Int16(bitPattern: UInt16(raw)).littleEndian
let temp = Double(signed) / 10.0
let time = start.addingTimeInterval(Double(index * (intervalSec > 0 ? intervalSec : interval)))
result.append(Sample(time: time, temp: temp))
offset += 2
index += 1
}
return result
}
基于UIKit的温度曲线绘制方案
将解析出的采样点画成曲线,最直观的方式是用UIView子类配合Core Graphics。横轴映射时间差,纵轴映射温度范围。为了支持缩放查看,可以记录最小最大温度,将数值线性变换到视图高度内。绘制时先填背景网格,再描折线,最后在两端标注极值,这样现场人员一眼能看出是否超温。
如果采样点非常多,比如上千个,每次layoutSubviews都重画会卡顿。此时可以用UIGraphicsImageRenderer把曲线先渲染成UIImage并缓存,只有数据更新时才重绘。另一个细节是用户可能在深色模式下来看,绘制代码应读取traitCollection.userInterfaceStyle切换线条颜色,避免白线白底看不见。
下面给出一个简化的绘制片段,它接收Sample数组并画出自适应折线。实际项目里还可以叠加阈值线,当某段温度高于八度就标红,满足冷链报警需求。
class TempChartView: UIView {
var samples: [Sample] = []
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext(), samples.count > 1 else { return }
let temps = samples.map { $0.temp }
guard let minT = temps.min(), let maxT = temps.max() else { return }
let range = (maxT - minT) == 0 ? 1 : (maxT - minT)
let stepX = rect.width / CGFloat(samples.count - 1)
ctx.setStrokeColor(UIColor.systemBlue.cgColor)
ctx.setLineWidth(2)
ctx.beginPath()
for (i, s) in samples.enumerated() {
let x = CGFloat(i) * stepX
let y = rect.height - CGFloat((s.temp - minT) / range) * rect.height
if i == 0 { ctx.move(to: CGPoint(x: x, y: y)) }
else { ctx.addLine(to: CGPoint(x: x, y: y)) }
}
ctx.strokePath()
}
}
整体来看,Core NFC读取只是入口,真正的价值在NDEF负载的严谨解析与面向业务的可视化。只要处理好字节序、时间基准和绘制性能,iPhone就能替代专用手持机,成为现场温度审计的轻量工具。