@LiveModel
in SwiftData
Ok, so you’ve got a SwiftUI
view that displays a SwiftData
record. Maybe the model looks like this:
@Model final class Person {
var name: String
var friendCount: Int = 0
init(name: String) {
self.name = name
}
}
The view maybe looks like this:
struct PersonView: View {
var person: Person
var body: some View {
HStack {
Text(person.name)
Text("\(person.friendCount) friends")
}
}
}
If updates happen in our container’s mainContext
, the view will be updated. But what if an update happens in a background context? Like this?
Task {
// Use a background task to update the person's friendCount
let context = ModelContext(container)
// Assume we've got a `personID` from somewhere
let person: Person = context.registeredModel(for: personID)!
// Update the friend count
person.friendCount += 1
// Save the background context
try! context.save()
}
The view won’t be updated and we’ll look like fools.
Unfortunately there’s not really a way (afaik) to be updated when a SwiftData store changes, but there is a way to do it with CoreData. Read this post for more details. Or check out fatbobman/SwiftDataKit to be able to hook into what that blog post describes. Or keep reading to see how I’m using it. Or go have a cup of coffee, pet a cat, do whatever you want.
The @LiveModel
property wrapper
Let’s use some stuff from SwiftDataKit to implement a property wrapper that keeps our model up to date, no matter where updates happen from. (This is something marcoarment/Blackbird has and I thought was nice).
import Combine
import SwiftData
import SwiftDataKit
import NotificationCenter
import CoreData
// Keep a SwiftData record up to date
@propertyWrapper @Observable public final class LiveModel<T: PersistentModel> {
var _model: T
@MainActor public var wrappedValue: T {
get { _model }
set { _model = newValue }
}
var cancellable: AnyCancellable?
@MainActor public init(wrappedValue: T) {
self._model = wrappedValue
if let context = wrappedValue.modelContext {
self.cancellable = NotificationCenter.default.publisher(for: .NSManagedObjectContextDidSave).sink { [weak self] notification in
guard let userInfo = notification.userInfo, let self else {
return
}
if let updated = userInfo["updated"],
// Convert to an actual swift set
let set = (updated as? NSSet as? Set<NSManagedObject>),
// See if this update is for our model
let object = set.first(where: { $0.objectID.persistentIdentifier == self._model.id }),
// We know we have a persistent identifier because of the above check, so try to reload
// our model from its context.
let model: T = context.registeredModel(for: object.objectID.persistentIdentifier!)
{
// Update our model, so the Observation system can let the view know.
self._model = model
}
}
}
}
deinit {
cancellable?.cancel()
}
}
Now our model updates whenever it changes, even from a child context. I guess it’d be better if SwiftData
did this automatically and maybe the next version will, but for now I’ve found this useful. Maybe you will too? Maybe it’s a terrible idea? Is there some way to do this without reaching into CoreData? Lemme know.
You can check out a demo of @LiveModel here: https://github.com/nakajima/LiveModelDemo.