Being Sendable with SwiftData
In the previous article, I showed how to setup a ModelActor which accessed SwiftData on a non-MainActor.
We were left with an implementation that used a with
pattern which allowed us to use a non-Sendable object within a closure. In this article I'm going to expand on that by resolving the issue with non-Sendable PersistentModel objects.
Why PersistentModels aren't Sendable
One of the revelations I received from my sessions at WWDC 2024 was that PersistentModel
object are not Sendable
. This means a PersistentModel
cannot be passed from one actor to another. In other words, it's difficult to pass them from one function to another unless they are on the same actor so you must do whatever you want with them within that function. This also means - no holding references to a PersistentModel within an @Observable
object.
Well then how can one hold a reference to a PersistentModel
? This is where the persistentIdentifer
come in.
The Power of PersistentIdentifier
In a great article from Xu Yang (aka FatBobMan) he goes over the different identifiers in Core Data and Swift Data. In our particular case, we'll be using the PersistentIdentifier
from .persistentModelID
:
extension ModelContext {
func persistentModel<T>(withID objectID: PersistentIdentifier) throws -> T?
where T: PersistentModel {
if let registered: T = registeredModel(for: objectID) {
return registered
}
if let notRegistered: T = model(for: objectID) as? T {
return notRegistered
}
let fetchDescriptor = FetchDescriptor<T>(
predicate: #Predicate { $0.persistentModelID == objectID },
fetchLimit: 1`
` )
return try fetch(fetchDescriptor).first
}
}
This will work great but we if we used a Phantom Type to store the PersistentModel type we are fetching.
Introducing the Model
In an article from Majid Jabrayilov he introduces the concept of the Phantom Type:
A phantom type is a generic type that is declared but never used inside a type where it is declared.
Typically we use a generics we are using it because a property stores a value of that type. However a Phantom Type simply uses it to note how the object is to be used. In our case we are going to use the generic type to denote what PersistentModel
type we are fetching:
public struct Model<T: PersistentModel>: Sendable {
public let persistentIdentifier: PersistentIdentifier
public init(persistentIdentifier: PersistentIdentifier) {
self.persistentIdentifier = persistentIdentifier
}
}
extension Model {
public init(_ model: T) {
self.init(persistentIdentifier: model.persistentModelID)
}
}
What's particularly great about this Model
type, is that it's actually Sendable
! We can then store this in SwiftUI or Observable object. Now let's go back and use this with our ModelContext
:
extension ModelContext {
func getOptional<T>(_ model: Model<T>) throws -> T?
where T: PersistentModel {
try self.persistentModel(withID: model.persistentIdentifier)
}
}
}
Putting all this together from our last article, we can use this in our Database
type:
let itemModel : Model<Item>
database.withModelContext{ modelContext in
guard let item = modelContext.getOptional(itemModel) else {
// shouldn't happen 😣
}
item.timestamp = Date()
try modelContext.save()
}
I'm sure you're already thinking we should use this for some CRUD operations on our Database
type. Well in the next article we'll explore how to add that to our API.