在数据库发生改变时 Persistent History Tracking( 耐久化前史盯梢 )会向订阅者发送提示,开发者能够借此机会对同一数据库进行的修正做出呼应,包括其他运用、组件(同一个 App Group)和批处理使命。因为 SwiftData 集成了对耐久化前史盯梢功用的支持,无需编写额定的代码,订阅告诉、兼并业务等作业都会由 SwiftData 主动完结。
然而,在某些情况下,开发者或许希望自行呼应耐久化前史盯梢的业务,以取得更多的灵活性。本文将介绍怎么在 SwiftData 中经过耐久化前史盯梢调查特定数据改变的办法。
原文发表在我的博客wwww.fatbobman.com 。 因为技术文章需求不断的迭代,当时耗费了不少的精力在不同的平台之间来维持文章的更新。故从 2024 年起,新的文章将只发布在我的博客上。
为什么要自行呼应耐久化前史盯梢业务
SwiftData 中集成了对耐久化前史盯梢的支持,使视图能够及时正确地呼应数据改变,这对于来自网络、其他运用或小组件对数据的修正很有协助。但是,在某些情况下,开发者需求自行呼应耐久化前史盯梢业务,而不仅仅停留在视图层面。
自行呼应耐久化前史盯梢业务的原因如下:
- 处理与其他功用的集成:SwiftData 或许无法与某些功用或结构完全集成,例如 NSCoreDataCoreSpotlightDelegate,这时需求自行处理业务来调整 Spotlight 中的展示。
- 对特定数据改变执行操作:当数据改变时,开发者或许需求执行额定逻辑或操作,自行呼应能够仅针对改变的数据执行,从而下降操作本钱。
- 扩展功用:自行呼应能够给开发者更大的灵活性和扩展性,依据需求完成 SwiftData 现在无法完结的功用。
总之,自行呼应耐久化前史盯梢业务能够为开发者提供更多操作空间,来处理集成问题、特定数据改变、以及扩展功用。这能让开发者更好地利用耐久化前史盯梢,以满意各种需求。
Persistent History Tracking 在 Core Data 中的处理逻辑
在 Core Data 中处理耐久化前史盯梢涉及以下过程:
-
为不同的数据操作者(运用、小组件)设置不同的业务作者:能够运用
transactionAuthor
属性为每个数据操作者(运用、小组件)分配唯一的称号。这样能够区分不同的数据操作者,使每个操作者的业务能够被正确地标识。 -
在同享容器中保存每个数据操作者的最终获取业务的时刻戳:能够运用
UserDefaults
将每个数据操作者的最终获取业务的时刻戳保存在 App Group 的同享容器中的某个方位。这样能够在后续的处理中,依据时刻戳来获取从前次兼并后新产生的一切耐久化前史盯梢业务。 -
敞开耐久化前史盯梢功用并呼应告诉:在 Core Data Stack 中,需求启用耐久化前史盯梢功用,并注册对耐久化前史盯梢告诉的调查者。
-
获取新产生的耐久化前史盯梢业务:在接收到耐久化前史盯梢告诉后,能够依据上一次获取业务的时刻戳,从耐久化前史盯梢存储中获取新产生的业务。通常,只需求获取非当时数据操作者(运用、小组件)产生的业务。
-
处理业务:对获取的耐久化前史盯梢业务进行处理,例如将改变兼并到当时的视图上下文中。
-
更新最终获取时刻戳:在处理完业务后,将本次获取的最新业务的时刻戳设置为最终获取时刻戳,以便下次获取时只获取新的业务。
-
铲除已兼并的业务:在确保一切数据操作者都已处理完业务后,能够依据需求铲除已兼并的业务。
NSPersistentCloudContainer 会主动兼并来自网络的同步业务,开发者无需自行处理。
阅览 在 CoreData 中运用耐久化前史盯梢 一文,了解完好的完成细节。
Persistent History Tracking 在 SwiftData 中的特别之处
在 SwiftData 中运用耐久化前史盯梢与 Core Data 相似,但也有一些特别之处:
-
视图层面的数据兼并:SwiftData 能够主动处理视图层面的数据兼并,因而开发者无需手动处理业务的兼并操作。
-
业务铲除:为了确保在同一个 App Group 中的其他运用 SwiftData 的成员都能正确获取到业务,不对现已处理过的业务进行铲除。
-
时刻戳的保存:每个运用 SwiftData 的 App Group 成员只需自行保存其最终获取的时刻戳,无需统一保存在同享容器中。
-
业务处理逻辑:因为 SwiftData 采用了完全不同的并发编程方法,业务处理逻辑会放置在一个
ModelActor
中。该实例负责处理耐久化前史盯梢业务的获取和处理。 -
NSPersistentHistoryChangeRequest
中 的fetchRequest
为nil
:在 SwiftData 中,经过fetchHistory
创立的NSPersistentHistoryChangeRequest
中的fetchRequest
为nil
,因而无法经过谓词的方法对业务进行挑选。挑选过程将在内存中进行。 -
数据信息转换:耐久化前史盯梢业务中包括的数据信息为
NSManagedObjectID
,需求运用 SwiftDataKit 将其转换为PersistentIdentifier
,以便在 SwiftData 中进行进一步处理。
在下面的具体完成中会对部分注意事项进行更详细的说明。
具体完成
你能够在 此处 取得完好的演示代码。
声明 DataProvider
首要咱们将先声明一个 DataProvider,其间包括了 ModelContainer 以及用来处理耐久化前史盯梢的 ModelActor:
import Foundation
import SwiftData
import SwiftDataKit
public final class DataProvider: @unchecked Sendable {
public var container: ModelContainer
// a model actor to handle persistent history tracking transaction
private var monitor: DBMonitor?
public static let share = DataProvider(inMemory: false, enableMonitor: true)
public static let preview = DataProvider(inMemory: true, enableMonitor: false)
init(inMemory: Bool = false, enableMonitor: Bool = false) {
let schema = Schema([
Item.self,
])
let modelConfiguration: ModelConfiguration
modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: inMemory)
do {
let container = try ModelContainer(for: schema, configurations: [modelConfiguration])
self.container = container
} catch {
fatalError("Could not create ModelContainer: (error)")
}
}
}
因为 DataProvider 中仅有的两个存储属性的类型都契合 Sendable
协议,因而我将 DataProvider 也声明为 Sendable
。
为 ModelContext 的 transactionAuthor 命名
在演示中,为了只处理不由当时运用的 mainContext
所产生的业务,咱们需求为 ModelContext 的 transactionAuthor 命名。
extension DataProvider {
@MainActor
private func setAuthor(container: ModelContainer, authorName: String) {
container.mainContext.managedObjectContext?.transactionAuthor = authorName
}
}
// in init
do {
let container = try ModelContainer(for: schema, configurations: [modelConfiguration])
self.container = container
// 设置 mainContext 的 transactionAuthor 为 mainApp
Task {
await setAuthor(container: container, authorName: "mainApp")
}
} catch {
fatalError("Could not create ModelContainer: (error)")
}
声明处理耐久前史盯梢的 ModelActor
SwiftData 采用了愈加安全、优雅的并发编程方法,咱们将一切与耐久化前史盯梢有关的代码放置到一个 ModelActor 中。
阅览 SwiftData 中的并发编程 一文,把握并发编程的新办法。
import Foundation
import SwiftData
import SwiftDataKit
import Combine
import CoreData
@ModelActor
public actor DBMonitor {
private var cancellable: AnyCancellable?
// 最终前史业务时刻戳
private var lastHistoryTransactionTimestamp: Date {
get {
UserDefaults.standard.object(forKey: "lastHistoryTransactionTimestamp") as? Date ?? Date.distantPast
}
set {
UserDefaults.standard.setValue(newValue, forKey: "lastHistoryTransactionTimestamp")
}
}
}
extension DBMonitor {
// 呼应耐久前史盯梢告诉
public func register(excludeAuthors: [String] = []) {
guard let coordinator = modelContext.coordinator else { return }
cancellable = NotificationCenter.default.publisher(
for: .NSPersistentStoreRemoteChange,
object: coordinator
)
.map { _ in () }
.prepend(())
.sink { _ in
self.processor(excludeAuthors: excludeAuthors)
}
}
// 收到告诉后,处理交易
private func processor(excludeAuthors: [String]) {
// 获取自前次时刻戳后的一切业务
let transactions = fetchTransaction()
// 保存最新的时刻戳
lastHistoryTransactionTimestamp = transactions.max { $1.timestamp > $0.timestamp }?.timestamp ?? .now
// 挑选业务,排除一切由 excludeAuthors 产生的业务
for transaction in transactions where !excludeAuthors.contains([transaction.author ?? ""]) {
for change in transaction.changes ?? [] {
// 将业务的 change 发送给处理单元
changeHandler(change)
}
}
}
// 获取自前次处理以来一切新生成的业务
private func fetchTransaction() -> [NSPersistentHistoryTransaction] {
let timestamp = lastHistoryTransactionTimestamp
let fetchRequest = NSPersistentHistoryChangeRequest.fetchHistory(after: timestamp)
// 在 SwiftData 中,fetchHistory 创立的 fetchRequest.fetchRequest 为 nil,无法设置 predicate
guard let historyResult = try? modelContext.managedObjectContext?.execute(fetchRequest) as? NSPersistentHistoryResult,
let transactions = historyResult.result as? [NSPersistentHistoryTransaction]
else {
return []
}
return transactions
}
// Process filtered transactions
private func changeHandler(_ change: NSPersistentHistoryChange) {
// 经过 SwiftDataKit ,将 NSManagedObjectID 转换为 PersistentIdentifier
if let id = change.changedObjectID.persistentIdentifier {
let author = change.transaction?.author ?? "unknown"
let changeType = change.changeType
print("author:(author) changeType:(changeType)")
print(id)
}
}
}
在 DBMonitor 中,咱们只处理不是由 excludeAuthors 列表中成员所产生的业务。你能够依据需求设置 excludeAuthors,比如将当时 App 的一切 modelContext 的 transactionAuthor 都增加进去。
在 DataProvider 启用 DBMonitor:
// DataProvider init
do {
let container = try ModelContainer(for: schema, configurations: [modelConfiguration])
self.container = container
Task {
await setAuthor(container: container, authorName: "mainApp")
}
// 创立 DBMonitor,处理耐久化前史盯梢业务
if enableMonitor {
Task.detached {
self.monitor = DBMonitor(modelContainer: container)
await self.monitor?.register(excludeAuthors: ["mainApp"])
}
}
} catch {
fatalError("Could not create ModelContainer: (error)")
}
在 Xcode 的 Strict Concurrency Checking
设置为 Complete 的情况下( 为 Swift 6 做准备,对并发代码做严格审查),如果 DataProvider 不契合 Sendable ,会取得如下的警告信息:
Capture of 'self' with non-sendable type 'DataProvider' in a `@Sendable` closure
测试
至此,咱们现已完结了在 SwiftData 中对耐久化前史盯梢的呼应作业。为了验证效果,咱们将创立一个新的 ModelActor,经过它来创立新的数据( 不运用 mainContext )。
@ModelActor
actor PrivateDataHandler {
func setAuthorName(name: String) {
modelContext.managedObjectContext?.transactionAuthor = name
}
func newItem() {
let item = Item(timestamp: .now)
modelContext.insert(item)
try? modelContext.save()
}
}
在 ContentView 中,增加经过 PrivateDataHandler 创立数据的按钮:
ToolbarItem(placement: .topBarLeading) {
Button {
let container = modelContext.container
Task.detached {
let handler = PrivateDataHandler(modelContainer: container)
// 将 PrivateDataHandler 的 modelContext 的 transactionAuthor 设置为 Private,也能够不设置
await handler.setAuthorName(name: "Private")
await handler.newItem()
}
} label: {
Text("New Item")
}
}
运行运用后,点击右上角的 +
按钮,因为新数据是经过 mainContext 创立的( mainApp 在 excludeAuthors 名单中 ),因而,对应的业务并不会发送给 changeHandler
。而经过左上角 “New Item” 按钮创立的数据,其对应的 modelContext 并不在 excludeAuthors 名单中,changeHandler
会打印对应的信息。
总结
自行处理耐久化前史盯梢业务,能够让咱们在 SwiftData 正在活跃发展的今日完成更多高档功用,这或许能协助那些想运用 SwiftData 但又对功用受限仍有顾虑的开发者。
订阅我的电子周报 Fatbobman’s Swift Weekly,你将每周及时获取有关 Swift、SwiftUI、CoreData 和 SwiftData 的最新文章和资讯。
原文发表在我的博客wwww.fatbobman.com
欢迎订阅我的大众号:【肘子的Swift记事本】