Fetching Core Data objects into a SwiftUI list means an @FetchRequest with an NSPredicate format string and an NSSortDescriptor — both untyped, both checked only at runtime.
#The old way
import SwiftUI
import CoreData
struct RecipeListView: View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Recipe.createdAt, ascending: false)],
predicate: NSPredicate(format: "servings > %d", 2)
)
private var recipes: FetchedResults<Recipe>
var body: some View {
List(recipes) { recipe in
Text(recipe.title)
}
}
}
#The new way
import SwiftUI
import SwiftData
struct RecipeListView: View {
@Query(
filter: #Predicate<Recipe> { $0.servings > 2 },
sort: \Recipe.createdAt,
order: .reverse
)
private var recipes: [Recipe]
var body: some View {
List(recipes) { recipe in
Text(recipe.title)
}
}
}
#Why it matters
#Predicateis compiled and type-checked against the model's real property types — no format strings, no silent typos- Sorting takes a key path directly instead of a stringly-typed
keyPath:argument @Queryre-executes automatically whenever the underlyingModelContextchanges, so the list updates without manual refresh logic- Composes with SwiftUI's own
.searchableand other state-driven view modifiers without extra glue code
#Gotcha
@Query's filter and sort parameters are fixed at declaration time — you can't hand it a variable predicate built from view state directly in the property wrapper. To filter by something dynamic like search text, give the view a custom init that builds the #Predicate and assigns it via _recipes = Query(filter: ..., sort: ...). Also worth knowing: @Query re-runs on every context save, not just ones touching the queried type, so a view watching a large or frequently-written model can end up refetching more than expected — profile before assuming it's free.