DatabaseRegionObservation is a low-level API for observing changes to specific database regions without fetching values.
Overview
While ValueObservation automatically fetches fresh values, DatabaseRegionObservation only notifies you that a region has changed:
let observation = DatabaseRegionObservation(tracking: Player.all())
observation.start(in: dbQueue) { error in
print("Error: \(error)")
} onChange: {
print("The player table was modified")
// You decide when and how to fetch
}
Most applications should use ValueObservation instead. Use DatabaseRegionObservation only when you need fine-grained control over when values are fetched.
Creating Observations
tracking(_:)
Observes a single database region:
// Observe the entire player table
let observation = DatabaseRegionObservation(tracking: Player.all())
// Observe specific rows
let observation = DatabaseRegionObservation(
tracking: Player.filter(Column("teamId") == 1)
)
// Observe specific columns
let observation = DatabaseRegionObservation(
tracking: Player.select(Column("score"))
)
tracking(::)
Observes multiple regions:
let observation = DatabaseRegionObservation(
tracking: Player.all(),
Team.all()
)
Database Regions
A database region represents a set of database rows and columns:
Table Regions
// Entire table
DatabaseRegionObservation(tracking: Table("player"))
// All rows from a record type
DatabaseRegionObservation(tracking: Player.all())
Row Regions
// Specific rows by primary key
DatabaseRegionObservation(tracking: Player.filter(key: [1, 2, 3]))
// Rows matching a filter
DatabaseRegionObservation(tracking: Player.filter(Column("teamId") == 1))
Column Regions
// Specific columns
DatabaseRegionObservation(
tracking: Player.select(Column("name"), Column("score"))
)
Full Database
DatabaseRegionObservation(tracking: DatabaseRegion.fullDatabase)
Starting Observations
start(in:onError:onChange:)
Starts observing database changes:
The database queue or pool
Called when an error occurs
Called when the observed region changes
let observation = DatabaseRegionObservation(tracking: Player.all())
let cancellable = observation.start(
in: dbQueue,
onError: { error in
print("Error: \(error)")
},
onChange: {
print("Player table changed")
// Fetch fresh data when needed
}
)
Use Cases
Throttled Updates
Fetch values only when appropriate:
class PlayerManager {
private var needsRefresh = false
private var observation: DatabaseRegionObservation
private var cancellable: AnyCancellable?
init(dbQueue: DatabaseQueue) {
observation = DatabaseRegionObservation(tracking: Player.all())
cancellable = observation.start(
in: dbQueue,
onError: { _ in },
onChange: { [weak self] in
self?.needsRefresh = true
}
)
}
func refreshIfNeeded(_ db: Database) throws -> [Player] {
guard needsRefresh else {
return cachedPlayers
}
let players = try Player.fetchAll(db)
cachedPlayers = players
needsRefresh = false
return players
}
}
Coalescing Multiple Changes
let observation = DatabaseRegionObservation(tracking: Player.all())
var updateTimer: Timer?
let cancellable = observation.start(
in: dbQueue,
onError: { _ in },
onChange: {
// Coalesce rapid changes
updateTimer?.invalidate()
updateTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in
refreshUI()
}
}
)
Custom Scheduling
let observation = DatabaseRegionObservation(tracking: Player.all())
let queue = DispatchQueue(label: "updates")
let cancellable = observation.start(
in: dbQueue,
onError: { _ in },
onChange: {
queue.async {
// Fetch and process on custom queue
self.fetchAndProcess()
}
}
)
Combining with ValueObservation
Use both for different purposes:
// Automatic UI updates
let valueObservation = ValueObservation.tracking { db in
try Player.fetchCount(db)
}
let valueCancellable = valueObservation.start(
in: dbQueue,
onError: { _ in },
onChange: { count in
updateCountLabel(count)
}
)
// Manual refresh trigger
let regionObservation = DatabaseRegionObservation(tracking: Player.all())
let regionCancellable = regionObservation.start(
in: dbQueue,
onError: { _ in },
onChange: {
showRefreshIndicator()
}
)
DatabaseRegionObservation has very low overhead - it only tracks changes without fetching data.
Use narrow regions when possible to reduce unnecessary notifications:// Better - only notified when score changes
DatabaseRegionObservation(tracking: Player.select(Column("score")))
// Less efficient - notified for any column change
DatabaseRegionObservation(tracking: Player.all())
Limitations
DatabaseRegionObservation does not provide the changed values. You must fetch them yourself when notified.
Cancellation
let cancellable = observation.start(
in: dbQueue,
onError: { _ in },
onChange: { }
)
// Stop observing
cancellable.cancel()
Observations are automatically cancelled when the cancellable is deallocated.
Error Handling
let cancellable = observation.start(
in: dbQueue,
onError: { error in
if let dbError = error as? DatabaseError {
print("Database error: \(dbError.message ?? "")")
}
},
onChange: {
// Handle change
}
)
When an error occurs, the observation stops and must be recreated.
See Also