Performing Efficient Queries

In the last lesson, you were able to update the sample app so it could filter between 3 different types of data: All Recipes, just Beverages, or just BakedGoods. While this provided the necessary functionality, it wasn’t the optimal implementation. Let’s look at how you can make more powerful queries.

Use Predicates to Filter Results

A @Query by itself, with no other filtering directives, will return all values. While this works for small amounts of data, larger sets of data are tougher to navigate for the user. Luckily, @Querys can support predicates.

@Query(filter: #Predicate<MyModel> { $0.name == "Iced tea" }, sort: \Recipe.name)
  var recipes: [Recipe]

The predicate in the @Query applies a filter, represented here by the #Predicate<MyModel> { $0.name == "Iced tea" } code block, to the fetched results. Only entries that cause the predicate’s closure to return true will be returned.

This query, however, has a static predicate. It’s defined at compile time and can’t be changed at run time.

Why would you want a dynamic predicate? Predicates provide users with different “slices” of your data. You may want to sort on a name, a date range, a particular value, or even a boolean. You can design the app’s user interface to let the user choose which slice of data they want to see at runtime.

To make your predicates dynamic, you don’t declare them as part of the @Query. Instead, you create instances of Predicate and Query objects, and combine them outside of the macro. For Predicates, the code looks like this, similar to what it was in the @Query above:

Predicate<Recipe> predicate = #Predicate <Recipe> {
  // your conditional goes here
}

Within the braces, you provide the logic that determines which items should be returned by the query. If you wanted to look for any tea recipes you have, your query might look like this:

Predicate<Recipe> predicate = #Predicate <Recipe> {
  $0.name.contains("tea")
}

Since this predicate is defined outside a @Query macro, it can be changed at runtime. You’ll see that in the demo next segment.

The query is structured similarly. A view that contains the results of the query would have a structure like this:

@Query var recipes: [Recipe]

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

Here, the predicate is passed into the view’s init method and is used to initialize the _recipes object. Remember, @Query are state-like properties, so to set the value in the init you need to use _recipes. This query takes in the predicate as the filter argument, and sorts using the \.name in forward (ascending) order. In the demo video in the next segment you’ll learn how to put this into practice.

Use Complex Predicates to Filter Across Multiple Properties

Databases typically provide multiple columns for each row, which translates to multiple properties for each object stored in a table. This means that queries should be able to search over multiple properties. If you’ve ever used SQL, or any similar database language, you’ve seen clauses like this:

SELECT * FROM table_name WHERE val < 5 AND otherVal > 100

This statement fetches all rows from table_name that meets the criteria after the WHERE keyword. So how can we do this with SwiftData? It ends up very analogous to the SQL statement above. A Predicates closure is simply a boolean test for each element that could get returned, and those boolean tests can be composed of smaller test with && and || operators. For example:

Predicate<Recipe> predicate = #Predicate <Recipe> {
  $0.name.contains("tea") && $0.caffeinated == true
}

Here, 2 simple test, one for the name and one for whether the beverage is caffeinated or not, are combined with a && operator. You can also evaluate separate predicates by using the evaluate method:

Predicate<Recipe> namePredicate = #Predicate <Recipe> {
  $0.name.contains("tea")
}

Predicate<Recipe> caffeinePredicate = #Predicate <Recipe> {
  $0.caffeinated == true
}

Predicate<Recipe> fullPredicate = #Predicate <Recipe> {
  namePredicate.evaluate($0) && caffeinePredicate.evaluate($0)
}

Here, the different boolean checks are assigned to their own predicates, and in the fullPredicate closure, are again combined with a && operator, but each predicate is evaluated via the evaluate method.

Predicates can greatly narrow down the number of results returned from the query. What if you want to place a hard limit on the amount returned?

Limiting the Amount of Returned Values

Depending on your use case, you don’t need to return all the values of your query. For example, if you have a widget that just needs to show the next recipe on the schedule, you can restrict the number of returned values by setting the fetchLimit of a FetchRequest to 1:

fetchDesc.fetchLimit = 1

In the next segment, you’ll do that and add a complex predicate to the SwiftRecipes app to get some practice with what you just learned.

See forum comments
Download course materials from Github
Previous: Introduction Next: Predicates Demo