While the filtering solution from the last lesson was functionally correct, the code was not ideal. Let’s streamline the predicate code and set it up so additional predicates can be added in the future.
To code along with this demo, open the starter project for this lesson.
Basic Predicates
In ContentView.swift define a function to form a predicate based on the passed in RecipeType:
func getPredicate(for type: RecipeType) -> Predicate<Recipe>? {
// Base predicate for recipe type
let typePredicate: Predicate<Recipe>?
switch type {
case .all:
typePredicate = nil
case .bakedGoods:
typePredicate = #Predicate<Recipe> { $0 is BakedGood }
case .beverages:
typePredicate = #Predicate<Recipe> { $0 is Beverage }
}
return typePredicate
}
Here, the code has been removed from RecipeDetailView and new code that focuses on Predicates, not Querys, has been placed into ContentView. The state property recipeType is still indicating which segment of the Picker the user has chosen, but now a Predicate is formed as a result of that choice,
To make sure that property is correctly set, add an init method that sets _recipeType to the passed in RecipeType value.
init(recipeType: RecipeType) {
_recipeType = State(initialValue: recipeType)
}
Then, in the body, update the call to RecipeListView to take in the predicate, instead of the RecipeType from last lesson:
RecipeListView(predicate: getPredicate(for: recipeType))
Finally, update the init method and the properties of RecipeListView. Remove the Querys for BakedGoods and Beverages, and then update the init method to take in the Predicate and as long as it isn’t nil, use it to initialize the Query.
@Query var recipes: [Recipe]
init(predicate: Predicate<Recipe>?) {
self.predicate = predicate
if predicate != nil {
_recipes = Query(filter: predicate, sort: \.name, order: .forward)
}
}
Don’t forget to update calls to ContentView throughout the code.
This functionality should mirror what you had at the end of the last lesson. But this time, you can do more!
Complex Predicates
You can build upon the existing predicate constructed in getPredicate(for:) by adding in another predicate, this time including a string to match against. First, you need a way for the user to provide such a string. Add the following below the navigationTitle modifier in ContentView:
.searchable(text: $searchString, placement: .automatic, prompt:
"Search for a recipe")
.autocapitalization(.none)
Don’t forget to add a state property for the search string at the top of ContentView
@State private var searchString: String = ""
This will add a search field below the table, and prompt the user to insert a search string. The last modifier turns auto capitalization off.
Then, in getPredicate(for:), before the return add the following code block to see if the search string matches, in part, any of the properties of the Recipe:
// Search predicate (contains in name, instructions, or any ingredient's
// name or amount)
let trimmedSearch = searchString.trimmingCharacters(in:
.whitespacesAndNewlines)
let hasSearch = !trimmedSearch.isEmpty
let searchPredicate: Predicate<Recipe>? = hasSearch ?
#Predicate<Recipe> { recipe in
recipe.name.contains(trimmedSearch) ||
recipe.instructions.contains(trimmedSearch) ||
recipe.ingredients.contains { ingredient in
ingredient.name.contains(trimmedSearch) ||
ingredient.amount.contains(trimmedSearch)
}
} : nil
Here, the search string is trimmed for both whitespace and new lines, and a check is made to make sure the search string is not empty. If it’s not empty, a Predicate is built to see if any other Recipe properties, such as the instructions or the ingredients, contain that string. There are 2 possible distinct predicates, one for the RecipeType, and another for the search string. Now let’s combine them.
Replace the return typePredicate line at the end with a switch statement that lets you either use a simple or complex predicate, depending on what was provided:
// Compose predicates
switch (typePredicate, searchPredicate) {
case (nil, nil):
return nil
case (let typePredicate?, nil):
return typePredicate
case (nil, let searchPredicate?):
return searchPredicate
case (let typePredicate?, let searchPredicate?):
return #Predicate<Recipe> { typePredicate.evaluate($0)
&& searchPredicate.evaluate($0) }
}
If one of the predicates is nil, the other is used (unless both of them are nil, in which case there is no predicate). If both predicates are provided, they are combined with a double ampersand to provide a complex predicate, using the evaluate method on each predicate.
Now, refresh the canvas. There should be a search field at the bottom. You can either filter purely by type, as you did in the last lesson, by search string, or both.
Limiting the number of returned values
In addition to predicates, there is one more way to limit the number of items that get returned from a fetch. I have an idea for a Home Screen widget that shows me the next planned recipe I want to make. To support this, I need to add a plannedDate property to the Recipe class:
var plannedDate: Date?
init(name: String, summary: String = "", instructions: String = "",
ingredients: [Ingredient] = [], plannedDate: Date? = nil) {
self.name = name
self.summary = summary
self.instructions = instructions
self.ingredients = ingredients
self.plannedDate = plannedDate
}
static func dateAt6PM(daysFromNow: Int) -> Date {
let calendar = Calendar.current
let now = Date()
let targetDate = calendar.date(byAdding: .day, value: daysFromNow,
to: calendar.startOfDay(for: now))!
return calendar.date(bySettingHour: 18, minute: 0, second: 0,
of: targetDate)!
}
A dateAt6PM convenience method has also been added to help find the dinner time Date for a given daysFromNow. This helps initialize the data in the database.
To use these, pick some recipes from the sampleData array and add a plannedDate to them. plannedDate is an optional argument, so there is no need to have a date for everything.
static let sampleData = [
Recipe(
name: "Mom's Spaghetti",
summary: "A great old fashioned spagehtti",
instructions: "Cook the spaghetti according to package instructions.
In a large pan, heat olive oil over medium heat. Add the onion and
garlic and sauté until softened. Add the canned tomatoes, basil,
oregano, and salt. Simmer for 15 minutes. Add the cooked spaghetti
and toss to coat. Serve hot.",
ingredients: [
Ingredient(name: "Spaghetti", amount: "1 box"),
Ingredient(name: "Onion", amount: "1 medium, diced"),
Ingredient(name: "Garlic", amount: "4 cloves"),
Ingredient(name: "Olive oil", amount: "2 Tbsp"),
Ingredient(name: "Canned tomatoes", amount: "1 Large can"),
Ingredient(name: "Basil", amount: "5 leaves"),
Ingredient(name: "Oregano", amount: "2 Tbsp")
],
plannedDate: Recipe.dateAt6PM(daysFromNow: 2)
),
//....
For my widget, I need a convenience method that will return the next Recipe with a plannedDate.
Let’s investigate how this might happen using Xcode 26’s new #Playground macro. In SampleData.swift, add a #Playground block, and add a getUpcomingRecipes function. Inside this function, make a FetchDescriptor:
#Playground {
// Widget code to get next recipe to prepare
@MainActor
func getUpcomingRecipes() -> [Recipe] {
let now = Date()
var fetchDesc = FetchDescriptor(sortBy: [SortDescriptor(
\Recipe.plannedDate, order: .forward)])
// Use a constant for 'now' outside the predicate; only literal values are
// allowed inside #Predicate
fetchDesc.predicate = #Predicate { ($0.plannedDate ?? now) > now }
This FetchDescriptor sorts by the \Recipe.plannedDate key path, and the predicate checks to see if the plannedDate is in the future. In other words, it returns all recipes with a planned date, in ascending order from now.
Since I only need the first entry for my widget, I can restrict the fetchLimit to 1.
fetchDesc.fetchLimit = 1
The FetchDescriptor can then be used to perform a fetch on the modelContext, which is built from the SampleData shared model container.
let modelContext = ModelContext(SampleData.shared.modelContainer)
if let upcomingRecipes: [Recipe] = try? modelContext.fetch(fetchDesc) {
if let recipe = upcomingRecipes.first {
return [recipe]
}
}
return []
}
let nextRecipe = getUpcomingRecipes()[0]
print("next recipe to make is \(nextRecipe.name) \(String(describing:
nextRecipe.plannedDate!))")
}
Here, the canvas got in a weird state, so I went ahead and cleaned the project.
The #Playground macro lets you see the output of this code block in the canvas, just like #Preview lets you see a SwiftUI preview.
The ability to refine the data returned from your database, even down to the exact number needed for the task at hand, gives your user a great amount of flexibility to find just what they need in an ever growing sea of data.