1

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?

2
  • In theory, you could create an "equivalent" NSManagedObjectModel from a SwiftData ModelContainer.Schema, then read everything using CoreData. Commented Aug 10 at 8:25
  • To be fair, even in CoreData, you are still "manually specifying each type" in the xcdatamodeld file. In SwiftData, you would have already manually specified each type when creating your ModelContainer. You can reuse that if you declare a [any PersistentModel.Type] array, and you avoid having to specify all the types a second time. Commented Aug 10 at 8:32

1 Answer 1

1

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
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.