GRDB.swift 数据库观察完全指南:ValueObservation、DatabaseRegionObservation 与 TransactionObserver 实战解析
【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swift
数据库变化是驱动现代应用界面刷新的核心信号。本指南以 GRDB.swift 官方文档 DatabaseObservation.md 为主线,系统讲解 GRDB 面向应用开发的四大观察机制——ValueObservation(值观察)、DatabaseRegionObservation(区域观察)、afterNextTransaction(事务回调)以及底层TransactionObserver(事务观察者协议),并深入对应源码与测试,帮助读者掌握从"感知数据库变化"到"以最小成本刷新 UI"的完整实战方案。
观察体系总览:SQLite 通知能力在 GRDB 中的四种用法
GRDB 的观察能力建立在 SQLite 的一项原生特性之上:SQLite 会向其宿主应用通知数据库中执行的变化,以及事务的提交(commit)与回滚(rollback)。GRDB 将这一特性包装为四个由高到低、层层递进的使用层次(见 DatabaseObservation.md 的 Overview):
| 观察方式 | 观察粒度 | 典型场景 |
|---|---|---|
ValueObservation | 数据库值 | 监听玩家列表、统计数字等"值"的变化,拿到新鲜值直接渲染 |
DatabaseRegionObservation | 数据库区域 | 事务提交后立刻得到通知,且保证在其他线程写入之前回调 |
Database/afterNextTransaction(onCommit:onRollback:) | 单次事务 | 只在"某条数据成功落盘"后执行一次副作用(如启动定位监控) |
TransactionObserver | 底层事务与逐条变更 | 需要逐行、逐列、甚至在提交前介入的进阶需求 |
其中前三种高层次的 API 全部由第四种——TransactionObserver协议——在底层支撑实现,理解底层协议有助于你驾驭所有观察功能。
ValueObservation:观察数据库值的实时变化
ValueObservation是 GRDB 面向应用开发最常用的观察类型,它跟踪数据库请求结果的变化,并在数据库发生变化时通知新鲜值(见 Extension/ValueObservation.md)。
三步上手
保证唯一数据库连接常驻:观察期间必须保持一个
DatabaseQueue或DatabasePool处于打开状态,观察会持有该连接直到停止(见ValueObservation.swift源码中start(in:)的实现,连接由观察生命周期托底)。用闭包定义被观察的值:
// 跟踪所有玩家 let observation = ValueObservation.tracking { db in try Player.fetchAll(db) } // 等价简写 let observation = ValueObservation.tracking(Player.fetchAll)被观察的值没有类型限制:可以执行多次请求、跨多张表、甚至使用原生 SQL。源码中的ValueObservationTrackingMode枚举(见 ValueObservation.swift)区分了三种跟踪模式:显式常量区域(constantRegion)、从取回结果推导的常量区域(constantRegionRecordedFromSelection)、以及每次 fetch 都会变化的非恒定区域(nonConstantRegionRecordedFromSelection),后者典型如Player.fetchOne(db, id: Int.random(in: 1...1000))。
- 启动观察并处理结果:
let cancellable = observation.start(in: dbQueue) { error in // 处理错误 } onChange: { (players: [Player]) in print("Fresh players", players) }- 停止观察:调用
start返回的DatabaseCancellable的cancel();当该对象被释放时观察也会自动停止。
cancellable.cancel()消费方式的三个出口
ValueObservation可以被转换成AsyncSequence、Combine Publisher 或 RxSwift Observable(Rx 需借助社区库 RxGRDB):
- Async 序列(默认在协作线程池上调度,源码见
values(in:scheduling:bufferingPolicy:),ValueObservation.swift):
do { for try await players in observation.values(in: dbQueue) { print("Fresh players", players) } } catch { // 处理错误 }- Combine Publisher(默认异步调度在主队列,见
publisher(in:scheduling:),ValueObservation.swift):
let cancellable = observation.publisher(in: dbQueue).sink { completion in // 处理完成 } receiveValue: { (players: [Player]) in print("Fresh players", players) }行为契约:必须知道的六条规则
ValueObservation的行为遵循以下固定契约:
- 先通知初始值:观察启动时立即产生一次初始值,之后才是后续变化。
- 只通知已提交到磁盘的变化:回滚的事务不会产生任何通知。
- 默认每次变更都通知:只要被取回值的任意组成部分(列、行等)被修改就触发;可通过"指定跟踪区域"(见下文)收紧。
- 默认在主 Actor 上异步通知:初始值与后续错误、变化都在主 Actor 上异步投递;可通过调度参数改变。
- 允许合并通知:短时间内多次连续变更可能被合并为一次通知。
- 可能连续出现相同值:可用
removeDuplicates()过滤重复。
重要:
ValueObservation并不适合所有场景。如果应用必须处理每一次变化、不允许合并,或者需要在数据库文件被进一步修改之前处理变化,应改用DatabaseRegionObservation;如果需要在提交到磁盘之前介入,则应使用TransactionObserver(原文出处见 Extension/ValueObservation.md)。
调度(Scheduling):控制回调发生在哪条线程
默认情况下,错误与变化回调都是@MainActor隔离的(源码中@MainActor public func start(...)的默认参数为.mainActor,见 ValueObservation.swift):
let cancellable = observation.start(in: dbQueue) { error in // 此闭包运行在主 Actor } onChange: { value in // 此闭包运行在主 Actor print("Fresh value", value) }通过向start()传入scheduling参数可定制调度策略:
.immediate:全部值在主 Actor 上通知,且第一个值在观察启动时立即同步送达。这对图形应用极其有用——无需等待异步首值、无需实现空态或加载屏即可立即配置视图;但代价是首次 fetch 期间 UI 无响应,只建议用于非常快的数据库请求:
let cancellable = observation .start(in: dbQueue, scheduling: .immediate) { error in // 主 Actor 上调用 } onChange: { value in // 主 Actor 上调用 print("Fresh value", value) } // <- 执行到这里时 "Fresh value" 已经打印过了.async(onQueue:):在指定的 DispatchQueue 上异步调度。必须提供串行队列——并发队列(如DispatchQueue.global(qos: .default))会打乱新鲜值的通知顺序:
let myQueue: DispatchQueue let cancellable = observation .start(in: dbQueue, scheduling: .async(myQueue)) { error in // 在 myQueue 上异步调用 } onChange: { value in // 在 myQueue 上异步调用 print("Fresh value", value) }.task:在 Swift 协作线程池上异步调度,也是把观察转为 async 序列时的隐式调度器:
let sharedObservation = observation.shared(in: dbQueue, scheduling: .task) do { for try await players in sharedObservation.values() { print("Fresh players", players) } }此外scheduling还影响数据库 fetch 本身的执行线程:
.immediate下初始 fetch 总在观察启动时同步执行于主 Actor;- 默认
.async下初始 fetch 总是异步执行,绝不阻塞主线程; - 默认情况下,新鲜值在数据库被修改后立即fetch——主线程改库会在主线程触发 fetch。若要保证"新鲜值绝不在主线程 fetch",需要使用
DatabasePool配合tracking(regions:fetch:)或trackingConstantRegion(_:)创建的优化观察(使用前务必通读这两个方法的文档,否则可能漏掉某些数据库变化)。
共享观察:一份 fetch,多点通知
共享ValueObservation可以节省数据库资源:当一次数据库变化发生时,新鲜值只 fetch 一次,然后广播给所有客户端(见shared(in:scheduling:extent:)):
// SharedValueObservation<[Player]> let sharedObservation = ValueObservation .tracking { db in try Player.fetchAll(db) } .shared(in: dbQueue)ValueObservation与SharedValueObservation几乎等价,但后者没有map这类操作符;需要转换时可借助 Combine:
let cancellable = try sharedObservation .publisher() // 把共享观察转为 Combine Publisher .map { ... } // 使用 Combine 的 map .sink(...)指定跟踪区域:观察与取回解耦
标准的tracking(_:)是"跟踪取回的值",而tracking(region:_:fetch:)让你把被观察的区域与被取回的值完全分离。例如,只在玩家score列变化时去取回 id=1 玩家的整行:
let observation = ValueObservation.tracking( // 定义被跟踪的数据库区域(player 表 id=1 的 score 列) region: Player.select(\.score).filter(id: 1), // 定义该区域变化后要取回什么(id=1 的玩家) fetch: { db in try Player.fetchOne(db, id: 1) } )任何遵循DatabaseRegionConvertible的类型——FetchRequest、DatabaseRegion、Table等——都可作为跟踪区域。DatabaseRegion的底层实现见 DatabaseRegion.swift:它内部以[CaseInsensitiveIdentifier: TableRegion]描述"哪些表 × 哪些列 × 哪些行号",且表名不区分大小写;nil字典则代表覆盖整个数据库的fullDatabase区域。
无法被自动检测的变化:notifyChanges(in:)
以下场景ValueObservation不会自动 fetch 并通知新鲜值:
- 外部数据库连接执行的变化;
- 非 GRDB 编译执行的
DELETE/INSERT/UPDATE之外的 SQLite 语句造成的变化; - 数据库 schema 变更、
sqlite_master等内部系统表的变更; WITHOUT ROWID表上的变更。
遇到上述情况,可在写事务中显式调用Database/notifyChanges(in:)手动通知:
try dbQueue.write { db in // 通知观察:数据库发生了某些变化 try db.notifyChanges(in: .fullDatabase) // 通知观察:player 表发生了变化 try db.notifyChanges(in: Player.all()) // 等价写法 try db.notifyChanges(in: Table("player")) }性能要点:减少数据库争用
ValueObservation的触发依据是"可能影响被跟踪值的事务",精确地说它跟踪的是DatabaseRegion而非值本身。例如跟踪玩家的最高分时,任何触及player.score列的事务(无论插入、更新还是删除)都会触发观察,即便最高分并未改变。这是性能考量的关键认知(原文出处见 Extension/ValueObservation.md):
- 活跃的观察会占用受限的数据库资源,触发时 fetch 新鲜值,可能延迟其他组件的读写访问,造成数据库争用。GRDB 给出的优化建议是:
- 控制观察数量:不要对列表中的每个元素单独建观察,而应把整个列表放进一个观察里;
- 及时停止观察:例如
UIViewController在viewWillAppear启动观察、viewWillDisappear停止;SwiftUI 应用可借助 GRDBQuery 的View.queryObservation(_:); - 尽量共享观察:
ValueObservation.start的每次调用都会触发独立的值刷新,多个组件关注同一值时用shared(in:scheduling:extent:); - 用
map(_:)做后处理:把"取原始值 + 计算"拆成tracking加map,map的执行不阻塞数据库访问也不阻塞主线程:
// 普通写法(fetch 与计算耦合在事务内) let observation = ValueObservation.tracking { db -> MyValue in let players = try Player.fetchAll(db) return computeMyValue(players) } // 优化写法(计算移出 fetch) let observation = ValueObservation .tracking { db try Player.fetchAll(db) } .map { players in computeMyValue(players) }- 截断型 WAL checkpoint 的副作用:在
DatabasePool上、且数据库缺失 wal 文件时启动观察,即使内容未变化也会在启动时通知两次值——因为无法创建检测启动期间变化所需的 wal 快照。若应用执行截断型 checkpoint(Database/checkpoint(_:on:)或PRAGMA wal_checkpoint),可在启动观察前用一个空事务(如创建再丢弃临时表)重建非空的 wal 文件来规避。
DatabaseRegionObservation:事务级区域观察
当应用需要在处理完每一次影响指定区域的事务后立刻得到通知,且保证"通知发生在任何其他线程有机会进一步写入之前"时,应使用DatabaseRegionObservation(见 Extension/DatabaseRegionObservation.md)。
文档特别强调:这种"提交后、其他人写入前"的强保证大多数应用并不真正需要,它们更想要的是新鲜值——所以先评估
ValueObservation是否满足需求,再决定是否使用本 API。
用法
用一条或多条请求创建观察,然后从DatabaseQueue/DatabasePool启动:
// 跟踪完整的 player 表 let observation = DatabaseRegionObservation(tracking: Player.all()) let cancellable = try observation.start(in: dbQueue) { error in // 处理错误 } onChange: { (db: Database) in print("Players were changed") }写库即触发通知:
try dbQueue.write { db in try Player(name: "Arthur").insert(db) } // 打印 "Players were changed"同样可用cancellable.cancel()或释放对象来停止;同样可转成 Combine Publisher(Rx 借助 RxGRDB)。DatabaseRegionObservation接受任何DatabaseRegionConvertible类型作为跟踪目标:
// 观察 player 表的 score 列(QueryInterface 写法) let observation = DatabaseRegionObservation( tracking: Player.select(\.score)) // 观察 player 表的 score 列(原生 SQL 写法) let observation = DatabaseRegionObservation( tracking: SQLRequest("SELECT score FROM player")) // 同时观察 player 与 team 两张表 let observation = DatabaseRegionObservation( tracking: Table("player"), Table("team")) // 观察整个数据库 let observation = DatabaseRegionObservation( tracking: .fullDatabase)DatabaseRegionObservation同样存在"无法检测的变化"清单(外部连接、非 GRDB 语句、schema 变更、WITHOUT ROWID表),处理方式与ValueObservation一致——在写事务中调用db.notifyChanges(in:)显式通知。相关测试可参考 Tests/GRDBTests/Core/DatabaseRegionObservationTests.swift 与 Combine 版 Tests/GRDBTests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift。
afterNextTransaction:只关心"这一次事务"是否成功
Database/afterNextTransaction(onCommit:onRollback:)用于在下一个或当前事务完成时执行一次回调,非常适合把数据库与其他资源(文件、系统服务)同步。
典型场景:落盘成功才启动副作用
源码文档给出的经典示例(见 TransactionObserver.swift)是CLLocationManager的监控启动——只有当CLRegion成功写入数据库并提交后才开始监控:
/// 向数据库插入 region,并在成功插入后开始监控。 func startMonitoring(_ db: Database, region: CLRegion) throws { // 确保数据库位于事务(savepoint)内 try db.inSavepoint { // 把 region 存入数据库 try insert(...) // 当且仅当插入最终被提交时开始监控 db.afterNextTransaction { _ in // locationManager 偏好主队列: DispatchQueue.main.async { locationManager.startMonitoring(for: region) } } return .commit } }如果事务最终回滚(显式回滚或因错误回滚),onCommit不会被执行,从而避免"数据库没存上却启动了监控"的不一致状态。
实现与约束
从源码看,afterNextTransaction本质上是把一个内部TransactionHandler观察者以.nextTransaction的 extent 注册到连接上(见 TransactionObserver.swift),TransactionHandler忽略所有变更事件,只在databaseDidCommit/databaseDidRollback中触发回调。使用注意:
onCommit与onRollback在写入分派队列上串行执行,与其他数据库更新互斥;- 前置条件:数据库连接不能是只读的(源码中
GRDBPrecondition(!isReadOnly, ...)会直接触发断言,因为只读事务不会被通知给观察者); onRollback有默认空实现,可按需只提供onCommit。
TransactionObserver:支撑一切的底层协议
TransactionObserver是所有观察功能的底层支撑(见 Extension/TransactionObserver.md)。它在事务提交到磁盘之前就通知逐条变更(插入、更新、删除),并在提交、回滚时给出对应回调。
注册观察者
观察者通过DatabaseWriter/add(transactionObserver:extent:)(作用于队列/池)或Database/add(transactionObserver:extent:)(作用于连接)注册:
let observer = MyObserver() dbQueue.add(transactionObserver: observer)默认情况下数据库对观察者持有弱引用:观察者不被保留,一旦被释放即自动停止接收通知。
回调时序:提交与回滚的完整生命周期
变更通过databaseDidChange(with:)通知(包括外键ON DELETE/ON UPDATE与 SQL 触发器引发的间接变更);事务完成通过databaseWillCommit()、databaseDidCommit(_:)、databaseDidRollback(_:)通知。被通知的变更在事务提交(databaseDidCommit)之前并未真正写入磁盘,databaseDidRollback则确认其失效:
try dbQueue.write { db in try db.execute(sql: "INSERT ...") // 1. didChange try db.execute(sql: "UPDATE ...") // 2. didChange } // 3. willCommit, 4. didCommit try dbQueue.inTransaction { db in try db.execute(sql: "INSERT ...") // 1. didChange try db.execute(sql: "UPDATE ...") // 2. didChange return .rollback // 3. didRollback } try dbQueue.write { db in try db.execute(sql: "INSERT ...") // 1. didChange throw SomeError() } // 2. didRollback显式事务之外的语句也不会漏掉:
try dbQueue.writeWithoutTransaction { db in try db.execute(sql: "INSERT ...") // 1. didChange, 2. willCommit, 3. didCommit try db.execute(sql: "UPDATE ...") // 4. didChange, 5. willCommit, 6. didCommit }Savepoint 场景:被挂起(savepoint 内部)的变更只有在其 release 之后才被通知,确保通知的事件都是"有机会被提交"的事件:
try dbQueue.inTransaction { db in try db.execute(sql: "INSERT ...") // 1. didChange try db.execute(sql: "SAVEPOINT foo") try db.execute(sql: "UPDATE ...") // 延迟 try db.execute(sql: "UPDATE ...") // 延迟 try db.execute(sql: "RELEASE SAVEPOINT foo") // 2. didChange, 3. didChange try db.execute(sql: "SAVEPOINT bar") try db.execute(sql: "UPDATE ...") // 不通知 try db.execute(sql: "ROLLBACK TO SAVEPOINT bar") try db.execute(sql: "RELEASE SAVEPOINT bar") return .commit // 4. willCommit, 5. didCommit }databaseWillCommit抛错会把错误暴露给应用代码:
do { try dbQueue.inTransaction { db in ... return .commit // 1. willCommit(抛错), 2. didRollback } } catch { // 3. 事务观察者抛出的错误 }两条硬性约束:
- 所有回调都在写入分派队列中调用,与所有数据库更新串行化;
databaseDidChange与databaseWillCommit回调不得以任何方式访问被观察的写入连接;该限制不适用于databaseDidCommit/databaseDidRollback(它们可以使用传入的Database参数)。
事件过滤:observes(eventsOfKind:)
默认的事件过滤机制是observes(eventsOfKind:)——它在数据库查询执行前只调用一次,即可完全禁用变更跟踪,是最高效、被推荐的过滤方式:
// 调用 observes(eventsOfKind:) 一次。 // 之后要么为每一行更新调用 databaseDidChange(with:),要么完全不调用。 try db.execute(sql: "UPDATE player SET score = score + 1")DatabaseEventKind参数能区分插入/删除/更新,并告知将要被修改的列。例如只关注player表:
class PlayerObserver: TransactionObserver { func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { // 只观察 "player" 表上的变化 eventKind.tableName == "player" } func databaseDidChange(with event: DatabaseEvent) { // 此方法只会在 "player" 表变化时被调用 } }即使对所有事件类型都返回false,观察者仍然会收到事务通知。该过滤机制也意味着观察者感知不到非 GRDB 编译执行的语句造成的变化——可通过实现databaseEventObservationStrategy解除此限制。
观察范围(Extent):观察者能活多久
可通过remove(transactionObserver:)随时显式停止,也可用add(transactionObserver:extent:)的 extent 参数指定生命周期(三种取值定义见 TransactionObserver.swift):
let observer = MyObserver() // 数据库队列或池上: dbQueue.add(transactionObserver: observer) // 默认 extent dbQueue.add(transactionObserver: observer, extent: .observerLifetime) dbQueue.add(transactionObserver: observer, extent: .nextTransaction) dbQueue.add(transactionObserver: observer, extent: .databaseLifetime) // 数据库连接上: dbQueue.inDatabase { db in db.add(transactionObserver: ...) }.observerLifetime(默认):数据库持有弱引用,观察者被释放即自动结束,期间通知所有变化与事务;.nextTransaction:激活到当前或下一个事务完成,数据库对其保持强引用,直到databaseDidCommit或databaseDidRollback被调用后不再通知;.databaseLifetime:数据库保留并通知观察者,直到连接关闭。
此外,观察者还可以在事务中途"退场":调用stopObservingDatabaseChangesUntilNextTransaction()后,databaseDidChange在当前事务结束前不再被调用——典型用于"只关心事务内是否出现过某类变化":
class PlayerObserver: TransactionObserver { var playerTableWasModified = false func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { eventKind.tableName == "player" } func databaseDidChange(with event: DatabaseEvent) { playerTableWasModified = true // 没必要继续跟踪后续变化: stopObservingDatabaseChangesUntilNextTransaction() } }进阶:SQLite Pre-Update Hooks
当 SQLite 以SQLITE_ENABLE_PREUPDATE_HOOK选项编译时,TransactionObserver会获得一个额外回调databaseWillChange(with:),可在变更发生前观察被修改行的各列初始值/最终值:
protocol TransactionObserver: AnyObject { #if SQLITE_ENABLE_PREUPDATE_HOOK /// 在数据库变更(插入、更新或删除)之前,附带变更信息 /// (该行各列的初始值 / 最终值)进行通知。 /// /// 事件仅在本次方法调用期间有效;如需保存请复制:event.copy()。 func databaseWillChange(with event: DatabasePreUpdateEvent) #endif }启用该能力的两种途径(详见 Extension/TransactionObserver.md):
- CocoaPods + 自定义编译选项:在 Podfile 的
post_install中为 GRDB.swift target 追加 Swift 与 C 编译定义:
pod 'GRDB.swift' post_install do |installer| installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target| target.build_configurations.each do |config| # 启用额外的 GRDB API config.build_settings['OTHER_SWIFT_FLAGS'] = "$(inherited) -D SQLITE_ENABLE_PREUPDATE_HOOK" # 启用额外的 SQLite API config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = "$(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1" end end end警告:务必使用匹配的平台版本!在较低版本设备上会触发运行时错误。 注意:
GCC_PREPROCESSOR_DEFINITIONS中的GRDB_SQLITE_ENABLE_PREUPDATE_HOOK=1用于补齐系统<sqlite3.h>缺失的 C 函数原型;一旦 Xcode SDK 自带完整头文件而出现重复定义编译错误,只需移除该项。
- 自定义 SQLite 构建:参考 CustomSQLiteBuilds.md,在编译 SQLite 时激活
SQLITE_ENABLE_PREUPDATE_HOOK选项。
底层观察者的"盲区"清单
TransactionObserver无法自动通知的变化与事务包括:
- 只读事务;
- 外部数据库连接执行的变化与事务;
- 非 GRDB 编译执行的
DELETE/INSERT/UPDATE之外的语句(可通过实现databaseEventObservationStrategy解除); - 数据库 schema 变更、
sqlite_master等内部系统表变更; WITHOUT ROWID表上的变更;- 由
ON CONFLICT REPLACE触发的重复行删除(此例外未来 SQLite 版本可能改变)。
与高层 API 相同的补救手段是显式调用Database/notifyChanges(in:),此时databaseDidChange()回调会被触发:
try dbQueue.write { db in // 通知观察者:数据库发生了某些变化 try db.notifyChanges(in: .fullDatabase) // 通知观察者:player 表发生了变化 try db.notifyChanges(in: Player.all()) // 等价写法 try db.notifyChanges(in: Table("player")) }若要通知 schema 变化,可通知sqlite_master表:
try dbQueue.write { db in // 通知所有观察者:sqlite_master 表变化 try db.notifyChanges(in: Table("sqlite_master")) }选型总结:四种观察方式怎么选
面对观察需求,可按下述路径决策(对应 DatabaseObservation.md 的 Topics 分类):
- 要新鲜值、要刷新 UI→
ValueObservation(或共享版SharedValueObservation、异步版AsyncValueObservation),配合Database/registerAccess(to:)进行访问注册; - 要在"每次影响指定区域的事务提交后、其他写入前"被回调,且能容忍自己手动取数 →
DatabaseRegionObservation; - 只想在"某次事务落盘"后执行一次副作用→
Database/afterNextTransaction(onCommit:onRollback:); - 需要逐条变更、提交前介入、精细过滤或列级 pre-update 信息→ 直接实现
TransactionObserver协议,并通过Database/add(transactionObserver:extent:)/DatabaseWriter/add(transactionObserver:extent:)注册,用Database/remove(transactionObserver:)/DatabaseWriter/remove(transactionObserver:)注销,按需选择Database/TransactionObservationExtent控制生命周期。
所有区域相关的类型(DatabaseRegion、DatabaseRegionConvertible)都可直接用于DatabaseRegionObservation与ValueObservation的区域跟踪。从 ValueObservation.swift 的源码结构可以看到,ValueObservation通过可配置的ValueReducer与trackingMode组合出"值变化感知 + 区域跟踪"的完整能力,而这一切最终都落在TransactionObserver提供的提交/回滚生命周期之上。掌握这四层机制,你便拥有了从"数据库变化"到"界面刷新"之间的完整、可控的响应链路。
【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swift
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考