
概览
在今年的 WWDC 24 中,苹果将 SwiftData 升级为了 2.0 版本。其中对部分已有功能进行了增强的同时也加入了许多全新的特性,比如历史记录追踪(History Trace)、"墓碑"(Tombstone)等。

我们可以利用 History Trace 来跟踪 SwiftData 持久存储中数据的变化,利用令牌我们还可以进一步优化 SwiftData 的使用效率。
在本篇博文中,您将学到如下内容:
- 历史记录追踪机制简介
- 使用令牌(HistoryToken)过滤历史记录
- 删除过期的历史记录
相信有了令牌的加持,必将为 SwiftData 历史记录追踪锦上添花、百尺竿头!
闲言少叙,让我们马上开始 History Trace 的优化之旅吧!
Let's go!!!;)
1. 历史记录追踪机制简介
简单来说,今年苹果在 WWDC 24 上新祭出的历史记录追踪(History Trace)可以让我们更加轻松的监控 SwiftData 持久存储中数据的变化。如此一来,我们即可以非常 nice 的同步模型上下文之间、进程之间以及系统组件与 App 之间的数据变化了。
历史记录追踪(History Trace)机制专门用来查询 SwiftData 模型数据更改的历史记录,主要来说它有以下几种用途:
- 了解数据存储何时发生了更改?发生了什么更改?即使记录从数据库中被彻底删除之后仍然可以获取其部分信息("墓碑"机制);
- 了解如何使用该信息构建远程服务器同步;
- 处理进程外的更改事件;
在 SwiftData 2.0 中,苹果为模型上下文(ModelContext)新增了 fetchHistory() 以及一系列相关的方法专门为历史记录追踪功能而服务:

利用它们我们只需寥寥几行代码即可监听数据变动,仿佛"樽前月下":
swift
let mainContext = self.modelContext
var historyDesc = HistoryDescriptor<DefaultHistoryTransaction>()
historyDesc.predicate = #Predicate { trans in
trans.author == "BG"
}
let transactions = try! mainContext.fetchHistory(historyDesc)
for trans in transactions {
for change in trans.changes {
let modelID = change.changedPersistentIdentifier
guard let changedItem = mainContext.model(for: change.changedPersistentIdentifier) as? Item else { continue }
switch change {
case .insert(let historyInsert):
print("find insert")
// 处理记录插入
case .update(let historyUpdate):
print("find update")
// 处理记录更新
case .delete(let historyDelete):
print("find del")
// 处理记录删除
@unknown default:
fatalError()
}
}
}
在上面的代码中,我们主要做了这样几件事:
- 利用模型上下文的 author 属性排除了非后台 ModelContent 修改的历史记录;
- 通过 Change 记录的 changedPersistentIdentifier 属性抓取了修改后的托管对象;
- 根据具体的 Change 类型(新增、更改和删除)来做出妥善的后续处理;
虽然上面的代码没有任何问题,不过需要注意的是历史追踪记录本身也是需要存储在持久数据库中的。这意味着:随着 History Trace 的持续监听这些追踪记录会让数据库的体积变得不堪重负,更尴尬的是这些过期的"累赘"往往已经没有再使用的价值了。
那么我们该如何是好呢?
别着急,HistoryToken 可以为我们解忧排愁!
2. 使用令牌(HistoryToken)过滤历史记录
准确的说,历史令牌(HistoryToken)其实是一种协议:

因为我们往往都是与 SwiftData 中的默认存储(Store)打交道,所以我们需要使用系统提供的遵循 HistoryToken 协议的实体类型:DefaultHistoryToken。

默认历史令牌本身包含历史追踪记录发生的时间,并且其本身遵守可比较(Comparable)协议,所以我们可以比较两个令牌来判断它们的时效性。
swift
@State var historyToken: DefaultHistoryToken?
private func handleChangeInMainContext() {
let mainContext = modelContext
var historyDesc = HistoryDescriptor<DefaultHistoryTransaction>()
if let token = historyToken {
historyDesc.predicate = #Predicate { trans in
// 排除旧令牌对应的历史记录
trans.author == "BG" && trans.token > token
}
} else {
historyDesc.predicate = #Predicate { trans in
trans.author == "BG"
}
}
let transactions = try! mainContext.fetchHistory(historyDesc)
for trans in transactions {
for change in trans.changes {
// 处理具体的历史追踪记录
}
}
// 保存最后一个历史令牌
historyToken = transactions.last?.token
}
如上代码所示:我们在每次监听历史追踪记录后还不忘保存最后一个历史令牌。这样做的好处是,我们就可以在下一次抓取历史追踪记录时排除过期的记录了。
3. 删除过期的历史记录
虽然上面我们已经能够悠然自得的通过历史令牌来排除过期的历史追踪记录,但是这些"累赘"还仍然顽强的占据着 SwiftData 持久存储数据库的宝贵空间。长此以往,这些无用的历史记录可能会让我们的 App 臃肿不堪。
其实,SwiftData 提供了专门的 deleteHistory() 方法来删除指定的历史追踪记录:

一般情况下,在过去监听中已经被抓取过的历史追踪记录我们都可以统统删掉:
swift
extension ModelContext {
func deleteTransactions(before token: DefaultHistoryToken) throws {
var descriptor = HistoryDescriptor<DefaultHistoryTransaction>()
descriptor.predicate = #Predicate {
// 删除早于指定 token 的所有历史追踪记录
$0.token < token
}
try self.deleteHistory(descriptor)
}
}
可以看到,我们删除历史记录的代码非常简单直接:只需将数据库中比指定 token 要早的历史追踪记录删除即可。
现在,我们适配 SwiftData 的 App 在使用 History Trace 时变得又快又好,底层的数据库也始终保持着苗条的身材,棒棒哒!!!
总结
在本篇博文中,我们讨论了如何使用令牌进一步优化 SwiftData 2.0 中历史记录追踪机制的使用;我们随后还介绍了删除数据库中无用追踪记录的方法。
感谢观赏,再会啦!8-)