swiftcore-dataswiftdata

How Can I Retrieve All SwiftData Values at Runtime?


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?


Solution

  • 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
        }
    }