Context
I’m fetching a large number of items from a SwiftData store using a frequently-changing predicate. Traditional @Query setups did not provide the flexibility I wanted (specifically for rendering loading states), so I created a background actor to handle fetching the data:
@ModelActor
actor ThreadsafeBackgroundActor: Sendable {
func fetchData(_ predicate: Predicate? = nil) throws -> [CardView] {
let descriptor = if let p = predicate {
FetchDescriptor(predicate: p)
} else {
FetchDescriptor()
}
let cards = try context.fetch(descriptor)
return cards.map(CardView.init)
}
}
I’ve also got a view model calling the actor:
@Observable
class CardListViewModel {
enum State {
case idle
case loading
case failed(Error)
case loaded([CardView])
}
private(set) var state = State.idle
func fetchData(container: ModelContainer, predicate: Predicate) async throws -> [CardView] {
let service = ThreadsafeBackgroundActor(modelContainer: container)
return try await service.fetchData(predicate)
}
@MainActor func load(container: ModelContainer, filter: CardPredicate) async {
state = .loading
do {
let cards = try await fetchData(container: container, predicate: filter.predicate)
state = .loaded(cards)
} catch is CancellationError {
state = .idle
} catch {
state = .failed(error)
}
}
}
And I’ve got a task on my SwiftUI view to kick off the initial load:
.task(id: cardFilter) { // Reloads whenever the card filter changes! Good!
viewModel.load(container: context.container, filter: cardFilter)
}
This setup works excellently until anything in the database changes. Database updates (inserts, modifications, deletes) do not trigger my load function, even after calling context.save().
How can I make my load function re-run whenever the database changes?
Attempts and Research
-
I’ve attempted a brute-force route of introducing a
reloadCountstate variable, passed around wherever needed. Areas of the code that update the database incrementreloadCount, and a separate task in my list view watches the count:.task(id: cardFilter) { /* Same call */ } .task(id: reloadCount) { // Eww... viewModel.load(container: context.container, filter: cardFilter) }Not only is this strategy tedious and brittle, but it also runs the risk of calling
loadmultiple times unnecessarily (especially at the initial render time). -
I’ve looked into Swift’s streaming notification system. I’m fairly confident that
NSPersistentStoreRemoteChangeis what I want to watch. I just cannot figure out how/where to initialize that watcher.addObserverasks for Objective-C annotations. I don’t think that.publisher().sink { }is the solution either, because I want to kick off the mutating callviewModel.load()in the (escaping) closure.