SwiftData Fetching An Existing Object

How do you find an existing SwiftData model object from its persistent model identifier?

Last updated: Feb 20, 2024

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 when accessing the name property of the deleted country:

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.