SwiftData in iOS 26

Dec 10 2025 · Swift 6.2, iOS 26, macOS 26, Xcode 26

Lesson 03: Efficient Queries

Predicates Demo

Episode complete

Play next episode

Next

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Heads up... You’re accessing parts of this content for free, with some sections shown as obfuscated text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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
}
init(recipeType: RecipeType) {
  _recipeType = State(initialValue: recipeType)
}
RecipeListView(predicate: getPredicate(for: recipeType))
@Query var recipes: [Recipe]

init(predicate: Predicate<Recipe>?) {
  self.predicate = predicate
  if predicate != nil {
    _recipes = Query(filter: predicate, sort: \.name, order: .forward)
  }
}

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)
@State private var searchString: String = ""
// 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
// 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) }
}

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)!
  }
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)
  ),
  //....
#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 }
fetchDesc.fetchLimit = 1
    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!))")
}
See forum comments
Cinema mode Download course materials from Github
Previous: Performing Efficient Queries Next: Conclusion