How do you find an existing SwiftData model object from its persistent model identifier?
Note: I wrote this article based on Xcode 15 beta 6.
Persistent Identifiers
SwiftData shares the concept of a persistent identifier with Core Data. This is a stable identifier for an object that can be safely passed between threads or stored for later state restoration.
Core Data uses the NSManagedObjectID
type as a persistent identifier stored in the objectID
property of every managed object:
// Core Data
country.objectID // NSManagedObjectID
Note that to store a managed object ID you first transform it to a URI representation:
country.objectID.uriRepresentation()
// x-coredata://CA38E266-BC20-41BB-9F37-8C7E9B62449E/Country/p130
SwiftData model objects have a persistentModelID
property that is a struct of type PersistentIdentifier
that is already both Codable
and Sendable
:
// SwiftData
country.persistentModelID // PersistentIdentifer
Examining the persistent identifier in the debugger reveals the Core Data implementation details:
p country.persistentModelID
(SwiftData.PersistentIdentifier) {
id = (url = "x-coredata://452F3E9B-ACDC-486A-A330-C66FE272880D/Country/p215")
implementation = 0x000060000260ad00 {
storeIdentifier = "452F3E9B-ACDC-486A-A330-C66FE272880D"
isTemporary = false
URIRepresentation = "x-coredata://452F3E9B-ACDC-486A-A330-C66FE272880D/Country/p215"
primaryKey = "p215"
entityName = "Country"
managedObjectID = 0xbd337a2d472b0769 {}
}
}
Note: When creating a new model object the persistent identifier is temporary and only changes to the permanent identifier when you save the context.
Retrieving the Object
Given a persistent identifier, both Core Data and SwiftData have methods to return the object from the context:
// Core Data
let object = managedObjectContext.object(with: objectID)
// SwiftData
let object = modelContext.model(for: persistentIdentifier)
What’s surprising about both of these methods is that they do not return an optional object. What happens if they don’t find an object with a matching persistent identifier in the context? The SwiftData documentation is missing but the Core Data documentation provides an explanation:
If the context doesn’t recognize the specified object, this method returns a fault — a placeholder object that doesn’t load its properties until your code accesses them. The context then fetches the corresponding values from the persistent store and uses those values to turn the fault into a fully realized object.
So if the object is not in the context you get back a faulted object. That’s not a problem if the object exists in the persistent store. When you access the properties Core Data takes care of fetching the values from the store. The danger comes when the object doesn’t exist in the store:
When this method returns a fault, Core Data makes no attempts to verify the existence of the underlying object in the persistent store. If the object doesn’t exist when the context tries to the fetch the object’s values, the framework throws an exception.
This can be surprising and dangerous since you’ll get a crash if you access a faulted property for an object that’s no longer in the store. For example, this SwiftData code crashes in beta 5:
let identifier = country.persistentModelID
context.delete(country)
try context.save()
if let result = context.model(for: identifier) as? Country {
print(result.name) // Crash!
}
Note that a deleted object is still registered with the context until you save the context. If you remove the context save from the above code the result will return the deleted object. You can test for that condition by checking the isDeleted
flag of the object.
Checking The Context
If you only care about finding the object in the context you can check for a registered object:
// Core Data
managedObjectContext.registeredObject(for: objectID)
// SwiftData
modelContext.registeredModel(for: objectID)
Both these methods return an optional object which is nil
if the context doesn’t have an object with a matching persistent identifier. This can be useful for avoiding a fetch request when you expect the object to be in the context.
Existing Objects?
Most of the time I only want to get back an object when it exists in the context or the store. Core Data provides an extra method which only returns an existing object:
managedObjectContext.existingObject(with: objectID)
This method first searches the context for a registered object. If the context doesn’t contain the object it performs a fetch from the store. Unlike the object(with:)
method if the object doesn’t exist in the store it throws an error.
Unfortunately, SwiftData doesn’t yet provide this method but we can provide something similar ourselves.
SwiftData Finding An Existing Object
SwiftData does have a registeredModel(for:)
method to tell us if there’s a matching object in the context. If that doesn’t give us an object we can perform a fetch request using the persistent identifier:
extension ModelContext {
func existingModel<T>(for objectID: PersistentIdentifier)
throws -> T? where T: PersistentModel {
if let registered: T = registeredModel(for: objectID) {
return registered
}
let fetchDescriptor = FetchDescriptor<T>(
predicate: #Predicate {
$0.persistentModelID == objectID
})
return try fetch(fetchDescriptor).first
}
}
Notes:
- Unlike the Core Data method I’m returning
nil
if the object is missing from the context and the persistent store rather than throwing an error. - As mentioned earlier this will return a deleted object if it’s still in the context.