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
?
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.