swiftswiftdata

SwiftData fetch - filter with Predicate with format


This is a follow up to my previous question: SwiftData - fetch all entities

I also want to have a fetchAllEntities() method where I pass in a value and an attribute:

func fetchAllEntities<T>(with value: String, for attribute: String) throws -> [T] where T : PersistentModel  {
    let descriptor = FetchDescriptor<T>()
    let predicate = NSPredicate(format: "\(attribute) == %@", value)
        
    let objects = try modelContainer.mainContext.fetch(descriptor) // to do filter with predicate
            
    return objects
}

In CoreData I use an NSPredicate with format: let predicate = NSPredicate(format: "\(attribute) == %@", value) and add that to the fetchRequest.

How can I do this with SwiftData ?


Solution

  • I think it is more idiomatic that the method take a Predicate<M>, and let the caller pass in the condition using the #Predicate macro.

    func fetchModels<M>(filter: Predicate<M>) throws -> [M]
        where M: PersistentModel
    {
        let fetchDescriptor = FetchDescriptor<M>(predicate: filter)
        return try fetch(fetchDescriptor)
    }
    

    Usage:

    let results = fetchModels(filter: #Predicate<SomeModel> { x in x.someString == "some value" })
    

    It is also possible to make the signature more closely match the original CoreData function, taking in a key path and a value to match.

    func fetchModels<M, V>(attribute: any Sendable & KeyPath<M, V>, matching value: V) throws -> [M]
        where M: PersistentModel, V: Sendable & Equatable & Codable
    {
        let fetchDescriptor = FetchDescriptor<M>(predicate: Predicate { m in
            PredicateExpressions.build_Equal(
                lhs: PredicateExpressions.build_KeyPath(
                    root: PredicateExpressions.build_Arg(m),
                    keyPath: attribute
                ),
                rhs: PredicateExpressions.build_Arg(value)
            )
        })
        return try modelContainer.mainContext.fetch(fetchDescriptor)
    }
    

    Usage:

    let results: [SomeModel] = try context.fetchModels(attribute: \.someString, matching: "some value")
    

    Instead of using the #Predicate macro, you basically write whatever the macro would have generated (the build_XXX calls), since the key path you are filtering by is now dynamic.