适配 Swift 6 • Actor 架构 • 并发安全 • 类型安全
面向生产环境的 Core Data 持久化历史跟踪解决方案,完整支持 Swift 6 并发。
V2 是一次基于现代 Swift 并发的完全重写:
- ✅ 全面支持 Swift 6 —— 以并发安全为目标设计
- ✅ Actor 架构 ——
HookRegistryActor与TransactionProcessorActor确保线程安全 - ✅ 零内存泄漏 —— 没有保留环,生命周期清晰
- ✅ 数据竞争防护 —— 使用 Swift Testing 进行并发测试
- ✅ Hook 系统 —— 支持 Observer Hook 与 Merge Hook
- ✅ 现代 API —— 全面 async/await,Hook 使用 UUID 管理
迁移提示:V2 需要 iOS 17+/macOS 14+/Swift 6,迁移步骤与行为变化详见 迁移指南。
Use persistent history tracking to determine what changes have occurred in the store since the enabling of persistent history tracking. — Apple Documentation
启用 Persistent History Tracking 后,Core Data 会为以下来源的所有更改生成事务:
- 主应用
- 各类扩展(Widget、Share Extension 等)
- 自定义后台上下文
- CloudKit 同步(如启用)
PersistentHistoryTrackingKit 会自动:
- 📥 获取其他 author 的新事务
- 🔄 合并到指定 NSManagedObjectContext
- 🧹 清理过期事务
- 🎣 触发 Hook 供监控或自定义合并
想了解原理?
- 最低要求:iOS 17+ / macOS 14+ / Swift 6.0+
- 特点:Actor 架构、Hook 系统、全面并发安全
- 适用场景:面向现代系统的新项目
- 最低要求:iOS 13+ / macOS 10.15+ / Swift 5.5+
- 特点:成熟稳定,支持旧平台
- 适用场景:需要兼顾旧系统、暂未迁移 Swift 6
安装 V1
dependencies: [
.package(url: "https://github.com/fatbobman/PersistentHistoryTrackingKit.git", from: "1.0.0")
]或直接使用 version-1 分支:查看文档
如果你准备把已有的 V1 项目迁移到 V2,请先阅读迁移指南。
dependencies: [
.package(url: "https://github.com/fatbobman/PersistentHistoryTrackingKit.git", from: "2.0.0")
]import CoreData
import PersistentHistoryTrackingKit
// 1. 打开 Persistent History Tracking
let container = NSPersistentContainer(name: "MyApp")
let description = container.persistentStoreDescriptions.first!
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores { _, error in
if let error { fatalError("Failed to load store: \(error)") }
}
// 2. 设置本端 author
container.viewContext.transactionAuthor = "MainApp"
// 3. 初始化 Kit
let kit = PersistentHistoryTrackingKit(
container: container,
contexts: [container.viewContext],
currentAuthor: "MainApp",
allAuthors: ["MainApp", "WidgetExtension", "ShareExtension"],
userDefaults: .standard,
cleanStrategy: .byDuration(seconds: 60 * 60 * 24 * 7),
logLevel: 1
)完成后 Kit 会自动监听远程通知、合并外部事务并清理历史。
为 App 与扩展设置唯一 author:
container.viewContext.transactionAuthor = "MainApp"
widgetContext.transactionAuthor = "WidgetExtension"
batchContext.transactionAuthor = "BatchProcessor"然后在 Kit 中列出所有 author:
allAuthors: ["MainApp", "WidgetExtension", "BatchProcessor"]重要提示: 交易清理是可选的且开销很低。旧交易不会显著影响性能,无需激进清理 - 选择适合你应用的宽松间隔即可。
// 选项 1: 基于时间的清理(推荐)
// 每隔指定时间最多清理一次
cleanStrategy: .byDuration(seconds: 60 * 60 * 24 * 7) // 7 天
// 选项 2: 基于通知次数的清理
// 每隔 N 次通知清理一次(较少使用)
cleanStrategy: .byNotification(times: 10)
// 选项 3: 不自动清理(手动控制)
cleanStrategy: .none推荐策略:
- 大多数应用: 使用
.byDuration(seconds: 60 * 60 * 24 * 7)(7 天) - 提供良好平衡 - CloudKit 用户: 必须使用
.byDuration(seconds: 60 * 60 * 24 * 7)或更长间隔,避免NSPersistentHistoryTokenExpiredError - 频繁交易: 考虑
.byDuration(seconds: 60 * 60 * 24 * 3)(3 天) - 手动控制: 使用
.none,在特定事件时清理(App 进入后台等)
自动清理采用保守策略: 只有所有非 batch author 都已在共享 UserDefaults 中记录各自的
merge 时间戳后,Kit 才会执行清理。只要有任何一个必要 author 尚未完成 merge,自动清理就会跳过。
CloudKit 内部依赖持久化历史记录。如果历史清理过于激进,CloudKit 可能丢失其追踪标记,导致 NSPersistentHistoryTokenExpiredError(错误代码 134301),这可能会造成本地数据库清除和强制从 iCloud 重新同步。
使用 CloudKit 时务必使用足够长时间的基于时间的清理(7 天以上):
let kit = PersistentHistoryTrackingKit(
container: container,
currentAuthor: "MainApp",
allAuthors: ["MainApp", "WidgetExtension"],
cleanStrategy: .byDuration(seconds: 60 * 60 * 24 * 7), // CloudKit 至少 7 天
userDefaults: userDefaults
)注意: 默认情况下,Kit 不会清理由 NSPersistentCloudKitContainer(CloudKit 镜像)生成的交易,避免干扰 CloudKit 的内部同步。
如需最大灵活性,你可以完全控制清理时机:
let kit = PersistentHistoryTrackingKit(
// ... 其他参数
cleanStrategy: .none, // 禁用自动清理
autoStart: false
)
// 创建手动清理器
let cleaner = kit.cleanerBuilder()
// 在你选择的时间进行清理
// 例如:App 进入后台、使用量低时等
Task {
await cleaner.clean()
}
// 准备好后启动 Kit
kit.start()let hookId = await kit.registerObserver(
entityName: "Person",
operation: .insert
) { contexts in
for context in contexts {
print("新建 Person: \(context.objectIDURL)")
await Analytics.track(event: "person_created", properties: [
"timestamp": context.timestamp,
"author": context.author
])
}
}
await kit.removeObserver(id: hookId)
await kit.removeObserver(entityName: "Person", operation: .insert)
> ℹ️ 同一事务中若多次对同一实体执行同一操作,回调只触发一次,
> 但 `contexts` 数组会包含该事务中所有对应的 `HookContext`,可一次性处理。适合日志、统计、推送、缓存失效等场景。
await kit.registerMergeHook { input in
for transaction in input.transactions {
for context in input.contexts {
await context.perform {
// 自定义合并逻辑
}
}
}
return .goOn // 或 .finish 跳过默认合并
}Merge Hook 可以直接介入合并流水线,例如在应用 history transaction 时临时禁用 undoManager。
完整 Hook 指南:Docs/HookMechanismCN.md
| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
container |
NSPersistentContainer |
Core Data 容器 | 必填 |
contexts |
[NSManagedObjectContext]? |
需要合并的上下文 | viewContext |
currentAuthor |
String |
当前 author | 必填 |
allAuthors |
[String] |
参与合并的 author | 必填 |
includingCloudKitMirroring |
Bool |
是否包含 CloudKit author | false |
batchAuthors |
[String] |
只写入不合并的 author | [] |
userDefaults |
UserDefaults |
存储时间戳 | 必填 |
cleanStrategy |
TransactionCleanStrategy |
清理策略 | .none |
maximumDuration |
TimeInterval |
为未来清理就绪策略预留 | 7 天 |
uniqueString |
String |
UserDefaults key 前缀 | 自动生成 |
logger |
PersistentHistoryTrackingKitLoggerProtocol? |
自定义日志 | DefaultLogger |
logLevel |
Int |
日志级别 (0-2) | 1 |
autoStart |
Bool |
是否自动启动 | true |
// Observer Hook
func registerObserver(...) async -> UUID
func removeObserver(id:) async -> Bool
func removeObserver(entityName:operation:) async
func removeAllObservers() async
// Merge Hook
func registerMergeHook(before:callback:) async -> UUID
func removeMergeHook(id:) async -> Bool
func removeAllMergeHooks() asyncfunc start()
func stop()
func cleanerBuilder() -> ManualCleanerActorlet defaults = UserDefaults(suiteName: "group.com.yourapp")!
let kit = PersistentHistoryTrackingKit(
container: container,
currentAuthor: "MainApp",
allAuthors: ["MainApp", "WidgetExtension"],
userDefaults: defaults,
cleanStrategy: .byDuration(seconds: 60 * 60 * 24 * 7)
)struct MyLogger: PersistentHistoryTrackingKitLoggerProtocol {
func log(type: PersistentHistoryTrackingKitLogType, message: String) {
Logger.log(type, message)
}
}let hook1 = await kit.registerObserver(entityName: "Person", operation: .insert) { _ in print("Hook 1") }
let hook2 = await kit.registerObserver(entityName: "Person", operation: .insert) { _ in print("Hook 2") }
await kit.removeObserver(id: hook2) // 仅移除第二个
let hookA = await kit.registerMergeHook { _ in print("Hook A"); return .goOn }
let hookB = await kit.registerMergeHook(before: hookA) { _ in print("Hook B"); return .goOn }
// 执行顺序:Hook B → Hook A- iOS 17.0+
- macOS 14.0+
- tvOS 17.0+
- watchOS 10.0+
- Swift 6.0+
- Xcode 16.0+
测试现已验证可并行执行。测试基础设施会在内部串行化
NSPersistentContainer的创建,以规避 Core Data 在并发加载存储时的崩溃,同时保留 suite 级并行执行。
当前运行验证已覆盖 iOS 15+。 虽然 Package 声明支持更低版本系统,但在当前 Xcode 环境中,iOS 13 与 iOS 14 还没有完成运行时验证。
如果你正在 iOS 13 或 iOS 14 上使用此库:
- Package 当前声明支持这些系统版本,但维护者尚未在当前工具链环境中完成运行时验证。
- 如果你遇到问题,请在提交 issue 时附上设备型号、iOS 版本以及复现步骤。
- 如果此库在 iOS 13 或 iOS 14 上运行正常,也欢迎反馈,这将有助于提升对旧系统兼容性的信心。
./test.sh # 推荐脚本,自动启用并行测试如需手动运行,建议使用:
swift test --parallel
swift test --filter HookRegistryActorTests欢迎 PR!
git clone https://github.com/fatbobman/PersistentHistoryTrackingKit.git
cd PersistentHistoryTrackingKit
swift build
./test.shMIT,详见 LICENSE。
Fatbobman (肘子)
- Blog: fatbobman.com
- Newsletter: Fatbobman's Swift Weekly
- Twitter: @fatbobman
感谢 Swift 与 Core Data 社区对 V2 的反馈与贡献,特别感谢修复 undo manager、去重策略及 Swift 6 迁移的贡献者。
如果你觉得这个库对你有帮助,欢迎支持我的工作:
你的支持将帮助我继续维护和改进开源 Swift 库。谢谢!🙏