在数据库发生改变时 Persistent History Tracking( 耐久化前史盯梢 )会向订阅者发送提示,开发者能够借此机会对同一数据库进行的修正做出呼应,包括其他运用、组件(同一个 App Group)和批处理使命。因为 SwiftData 集成了对耐久化前史盯梢功用的支持,无需编写额定的代码,订阅告诉、兼并业务等作业都会由 SwiftData 主动完结。

然而,在某些情况下,开发者或许希望自行呼应耐久化前史盯梢的业务,以取得更多的灵活性。本文将介绍怎么在 SwiftData 中经过耐久化前史盯梢调查特定数据改变的办法。

原文发表在我的博客wwww.fatbobman.com 。 因为技术文章需求不断的迭代,当时耗费了不少的精力在不同的平台之间来维持文章的更新。故从 2024 年起,新的文章将只发布在我的博客上

为什么要自行呼应耐久化前史盯梢业务

SwiftData 中集成了对耐久化前史盯梢的支持,使视图能够及时正确地呼应数据改变,这对于来自网络、其他运用或小组件对数据的修正很有协助。但是,在某些情况下,开发者需求自行呼应耐久化前史盯梢业务,而不仅仅停留在视图层面。

自行呼应耐久化前史盯梢业务的原因如下:

  1. 处理与其他功用的集成:SwiftData 或许无法与某些功用或结构完全集成,例如 NSCoreDataCoreSpotlightDelegate,这时需求自行处理业务来调整 Spotlight 中的展示。
  2. 对特定数据改变执行操作:当数据改变时,开发者或许需求执行额定逻辑或操作,自行呼应能够仅针对改变的数据执行,从而下降操作本钱。
  3. 扩展功用:自行呼应能够给开发者更大的灵活性和扩展性,依据需求完成 SwiftData 现在无法完结的功用。

总之,自行呼应耐久化前史盯梢业务能够为开发者提供更多操作空间,来处理集成问题、特定数据改变、以及扩展功用。这能让开发者更好地利用耐久化前史盯梢,以满意各种需求。

Persistent History Tracking 在 Core Data 中的处理逻辑

在 Core Data 中处理耐久化前史盯梢涉及以下过程:

  1. 为不同的数据操作者(运用、小组件)设置不同的业务作者:能够运用transactionAuthor 属性为每个数据操作者(运用、小组件)分配唯一的称号。这样能够区分不同的数据操作者,使每个操作者的业务能够被正确地标识。

  2. 在同享容器中保存每个数据操作者的最终获取业务的时刻戳:能够运用UserDefaults将每个数据操作者的最终获取业务的时刻戳保存在 App Group 的同享容器中的某个方位。这样能够在后续的处理中,依据时刻戳来获取从前次兼并后新产生的一切耐久化前史盯梢业务。

  3. 敞开耐久化前史盯梢功用并呼应告诉:在 Core Data Stack 中,需求启用耐久化前史盯梢功用,并注册对耐久化前史盯梢告诉的调查者。

  4. 获取新产生的耐久化前史盯梢业务:在接收到耐久化前史盯梢告诉后,能够依据上一次获取业务的时刻戳,从耐久化前史盯梢存储中获取新产生的业务。通常,只需求获取非当时数据操作者(运用、小组件)产生的业务。

  5. 处理业务:对获取的耐久化前史盯梢业务进行处理,例如将改变兼并到当时的视图上下文中。

  6. 更新最终获取时刻戳:在处理完业务后,将本次获取的最新业务的时刻戳设置为最终获取时刻戳,以便下次获取时只获取新的业务。

  7. 铲除已兼并的业务:在确保一切数据操作者都已处理完业务后,能够依据需求铲除已兼并的业务。

NSPersistentCloudContainer 会主动兼并来自网络的同步业务,开发者无需自行处理。

阅览 在 CoreData 中运用耐久化前史盯梢 一文,了解完好的完成细节。

Persistent History Tracking 在 SwiftData 中的特别之处

在 SwiftData 中运用耐久化前史盯梢与 Core Data 相似,但也有一些特别之处:

  1. 视图层面的数据兼并:SwiftData 能够主动处理视图层面的数据兼并,因而开发者无需手动处理业务的兼并操作。

  2. 业务铲除:为了确保在同一个 App Group 中的其他运用 SwiftData 的成员都能正确获取到业务,不对现已处理过的业务进行铲除。

  3. 时刻戳的保存:每个运用 SwiftData 的 App Group 成员只需自行保存其最终获取的时刻戳,无需统一保存在同享容器中。

  4. 业务处理逻辑:因为 SwiftData 采用了完全不同的并发编程方法,业务处理逻辑会放置在一个ModelActor中。该实例负责处理耐久化前史盯梢业务的获取和处理。

  5. NSPersistentHistoryChangeRequest 中 的fetchRequestnil:在 SwiftData 中,经过 fetchHistory 创立的 NSPersistentHistoryChangeRequest 中的 fetchRequestnil,因而无法经过谓词的方法对业务进行挑选。挑选过程将在内存中进行。

  6. 数据信息转换:耐久化前史盯梢业务中包括的数据信息为 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 会打印对应的信息。

怎么经过 Persistent History Tracking 调查 SwiftData 的数据改变

总结

自行处理耐久化前史盯梢业务,能够让咱们在 SwiftData 正在活跃发展的今日完成更多高档功用,这或许能协助那些想运用 SwiftData 但又对功用受限仍有顾虑的开发者。

订阅我的电子周报 Fatbobman’s Swift Weekly,你将每周及时获取有关 Swift、SwiftUI、CoreData 和 SwiftData 的最新文章和资讯。

原文发表在我的博客wwww.fatbobman.com

欢迎订阅我的大众号:【肘子的Swift记事本】