I'm working with SwiftData
and trying to replicate behavior similar to what I used to do with CoreData
, where I had an extension on NSManagedObjectContext
that allowed me to fetch all stored objects, regardless of entity type.
In CoreData
, I used this:
extension NSManagedObjectContext {
public func fetchDebugData() throws -> [DebugEntity] {
guard let entities = persistentStoreCoordinator?.managedObjectModel.entities else {
return []
}
return try entities.compactMap { entity -> DebugEntity? in
guard let entityName = entity.name else { return nil }
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
let results = try fetch(fetchRequest)
let debugObjects = results.compactMap { object -> DebugObject? in
guard let managedObject = object as? NSManagedObject else { return nil }
let keysArray = Array(managedObject.entity.attributesByName.keys)
return DebugObject(
attributes: managedObject.dictionaryWithValues(forKeys: keysArray),
managedObject: managedObject
)
}
return DebugEntity(
entityName: entityName,
objects: debugObjects
)
}
}
}
public struct DebugEntity: Identifiable {
public let id = UUID()
public let entityName: String
public let objects: [DebugObject]
// MARK: - Initializer
public init(
entityName: String,
objects: [DebugObject]
) {
self.entityName = entityName
self.objects = objects
}
}
public struct DebugObject: Identifiable {
public let id = UUID()
public let attributes: [String: Any]
public let managedObject: NSManagedObject?
// MARK: - Initializer
public init(
attributes: [String: Any],
managedObject: NSManagedObject?
) {
self.attributes = attributes
self.managedObject = managedObject
}
}
I’m now using SwiftData
with @Model
types and the ModelContext
, and I want to dynamically fetch all objects from the store at runtime, without manually specifying each type. I understand that SwiftData
is designed to be more type-safe and less dynamic than CoreData
, but is there any way to accomplish this without traversing through the raw SQLite file manually?
There are no public APIs for getting all the model types in a ModelContainer
, but you can do it using Mirror
. Keep in mind that the names in the mirror paths could very possibly change in the future. Since this seems to be for debugging, I think it is still useful to demonstrate this approach.
extension Schema.Entity {
var metatype: any PersistentModel.Type {
let mirror = Mirror(reflecting: self)
return mirror.descendant("_objectType") as! any PersistentModel.Type
}
}
Now modelContext.container.schema.entities.map(\.metatype)
gets you a [any PersistentModel.Type]
.
Looping through that array, we can get a [any PersistentModel]
containing all the models.
extension ModelContext {
func allModels() throws -> [any PersistentModel] {
try container.schema.entities.map(\.metatype).flatMap { (type: any PersistentModel.Type) in
try fetchAll(type)
}
}
// helper for opening existentials
func fetchAll<T: PersistentModel>(_ type: T.Type) throws -> [any PersistentModel] {
try self.fetch(FetchDescriptor<T>())
}
}
Getting the values of the model properties is going to be even harder. The values you want are stored in a private property named _$backingData
. In this "backing data", there is an array array
storing the property values, as well as a lookup table lut
telling you which index of the array corresponds to which property.
You can see how this works more clearly if you dump
a PersistentModel
. Here is an excerpt for dump(Foo(name: "Foo"))
▿ Foo.Foo #0
- _name: Foo.Foo._SwiftDataNoType
▿ _$backingData: SwiftData._KKMDBackingData<Foo.Foo> #1
- super: SwiftData._InitialBackingData<Foo.Foo>
▿ _storage: KnownKeysDictionary:KnownKeysMap: ["name": 0] values: [Optional("Foo")] #2
▿ lut: KnownKeysMap: ["name": 0]
▿ backing: 1 key/value pair
▿ (2 elements)
- key: "name"
- value: 0
▿ keyPathBacking: 1 key/value pair
▿ (2 elements)
- key: \Foo.name #3
- super: Swift.WritableKeyPath<Foo.Foo, Swift.String>
- super: Swift.KeyPath<Foo.Foo, Swift.String>
- super: Swift.PartialKeyPath<Foo.Foo>
▿ super: Swift.AnyKeyPath
- _kvcKeyPathStringPtr: nil
- value: 0
▿ arr: 1 element
▿ Optional("Foo")
- some: "Foo"
Combining all of that, you can write:
extension ModelContext {
func fetchDebugArray() throws -> [(any PersistentModel.Type, [[String: Any]])] {
try fetchDebugArray(container.schema.entities.map(\.metatype))
}
func fetchDebugArray(_ types: [any PersistentModel.Type]) throws -> [(any PersistentModel.Type, [[String: Any]])] {
try types.map {
($0, try fetchDebugArray($0))
}
}
func fetchDebugArray<T: PersistentModel>(_ type: T.Type) throws -> [[String: Any]] {
let models = try self.fetch(FetchDescriptor<T>())
return models.compactMap(modelToDict)
}
private func modelToDict<T: PersistentModel>(_ model: T) -> [String: Any] {
let mirror = Mirror(reflecting: model)
let lut = mirror.descendant("_$backingData", "_storage", "lut", "backing") as! [String: Int]
let arr = mirror.descendant("_$backingData", "_storage", "arr") as! [Any?]
var retVal = [String: Any]()
for (key, index) in lut {
if let value = arr[index] {
retVal[key] = value
}
}
return retVal
}
}