SwiftUI Search: Getting Started

Learn how to use the searchable modifier to quickly add search capability to your SwiftUI apps. By Mina H. Gerges.

5 (4) · 3 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Clearing and Canceling a Search

In the previous section, you gave your user a search field where they can enter and submit a search query. Then, you filter the recipe list according to the query. However, when the user clears the query, the list doesn’t change. SwiftUI has .onChange(of:perform:) for this reason.

Inside RecipesView.swift, add this modifier to VStack before .onSubmit(of:_:):

.onChange(of: searchQuery) { _ in
  filterRecipes()
}

Now, any change to the search query triggers an update to the filtered list. This means that not only does clearing the search box or canceling your search reset the recipe’s list, but the list is actually filtered as the user types a query!

Build and run. Reveal the search field and start typing Roast, one letter at a time, without pressing Enter or tapping Search. Now you’re updating the list with each change in the search query.

The recipe list filters results as a user types

Now, clear the search query in the search bar and notice how it updates the recipe list, displaying the full list again. Next, try a new search, and then tap Cancel. See how the full list returns, as expected.

Note: You might feel a bit of redundancy in the code of .onChange(of:perform:) or .onSubmit(of:_:). Later in this tutorial, you’ll use an alternative approach to update the recipe list without both modifiers.

You can now offer Swifty an error-free search experience. But Swifty is special, and you’re a special developer who only offers the best. So, you’ll add advanced features in the following sections, starting with displaying search suggestions.

Swifty shows his love with hearts

Offering Search Suggestions

Setting the right prompt in the search field helps your user know what type of input they can search by. But offering search suggestions makes the process faster and simpler. searchable(text:placement:prompt:) has a parameter called suggestions that shows custom suggestions and controls what happens when the user selects any suggestion.

The suggestions appear as an overlay below the search bar, covering the list. You can display each suggestion as text, an image or any view you offer in the suggestions block. There are two ways to decide what happens when the user taps any suggestion. You’ll try them both now in your code.

Note: On tvOS, searchable modifiers only support suggestions of type Text.

Open RecipesView.swift. Add this code as the third parameter inside searchable(text:placement:prompt:) after prompt:

suggestions: {
  // 1
  Button("Pizza") {
    searchQuery = "pizza"
  }
  // 2
  Text("Chicken Salad")
    .searchCompletion("Chicken Salad")
}

With this code:

  1. You add this suggestion as Button view with the text “Pizza”. So you choose what happens when the user taps this suggestion in the action block of Button. Here, you change searchQuery to pizza.
  2. In the non-interactive views like Text, you associate it with searchCompletion(_:). When the user taps this suggestion, SwiftUI replaces the text inside the search field with the string inside searchCompletion(_:).

Build and run. Put the cursor in the search field and notice how the two search suggestions appear before you type anything. Select both suggestions, and check what happens in each case.

Search suggestion options, one using a button action and another using searchCompletion

Can you spot the difference between selecting each of the two suggestions?

Selecting either of them changes searchQuery, either directly or by changing the text inside the search field. Since .onChange(of:perform:) responds to changes in searchQuery, it filters the list in both selections by the recipe name using the new search text.

Because you tap the suggestion button in the first suggestion, it dismisses the suggestion list. But, when you select the second suggestion, searchCompletion(_:) removes the selected choice from the suggestion overlay, keeping the suggestion list onscreen.

Press Enter to dismiss the suggestions view and display the list behind it in the second selection.

Search field shows Chicken Salad and list below it filtered by name with this search query

Making Suggestions Dynamic

Now to make the suggestion list dynamic, replace the code inside suggestions from searchable(text:placement:prompt:) with the following:

// 1
ForEach(
  chefRecipesModel.nameSuggestions,
    id: \.self
) { suggestion in
  // 2
  Text(suggestion)
    .searchCompletion(suggestion)
}

Here’s what’s going on here:

  1. Instead of adding individual suggestions one by one, ForEach loops over the nameSuggestions array from ChefRecipesModel.swift.
  2. You display each suggestion as Text and associate it with searchCompletion(_:), like you did before.

Build and run. Check the new suggestions, select any one of them, then press Enter to make sure everything works as expected.

Selecting a search suggestion removes the item from the list, hitting Enter filters the recipes by the suggestion

Note: If the search query you type matches the string of any searchCompletion(_:), then SwiftUI removes the item from the suggestion list.

Swifty likes the suggestion feature you added, but he wants to search the recipes by their ingredients too. You’ll enable him to do this in the next sections.

Improving the Search Experience

Swifty would like the option to toggle between searching the list by a recipe’s name and by a recipe’s ingredients. You’ll start by creating a toggle switch.

First, add this @State property inside RecipesView.swift :

 @State var isSearchingIngredient = false

This sets a variable to track whether or not you’re searching by ingredients, setting it to false by default.

Next, add this Toggle inside VStack before List:

Toggle("**Search By Ingredients**", isOn:
  $isSearchingIngredient)
    .tint(Color("rw-green"))
    .foregroundColor(Color("rw-green"))
    .font(.body)
    .padding([.leading, .trailing])

This creates a toggle switch in your UI, the value of which tracks isSearchingIngredient‘s state.

Now, update prompt inside searchable(text:placement:prompt:) to show Swifty what he’s searching with the following code:

isSearchingIngredient ? "Search By Ingredient" :
  "Search By Meal Name"

Now, the prompt inside the search box changes based on if the user toggles on Search By Ingredients.

Build and run. Tap the toggle and see how the search field’s prompt changes:

Toggling Search By Ingredients changes the prompt in the search box

Now, you’ll create search lists according to the status of Search By Ingredients toggle. You’ll also create custom views to handle different suggestions views.

Creating Search Lists

Right-click the Recipe View folder and select New File…. Choose SwiftUI View, then click Next. Name the file as IngredientSuggestionView. Click Create.

Add this property to IngredientSuggestionView:

var chefRecipesModel = ChefRecipesModel()

This creates a new ChefRecipesModel() in your struct.

Next, replace body‘s content with the code below:

ForEach(
  chefRecipesModel.ingredientSuggestions,
  id: \.self) { ingredient in
    Text(ingredient)
      .searchCompletion(ingredient)
      .padding()
      .font(.title)
}

For each ingredientSuggestions, you create a Text view to represent that suggestion in the search completion list.

Similarly, create a new SwiftUI View in the Recipe View folder called NameSuggestionView. Then, add this property:

var chefRecipesModel = ChefRecipesModel()

And replace body‘s content with the code below:

ForEach(
  chefRecipesModel.nameSuggestions,
  id: \.self) { suggestion in
    Text(suggestion)
      .searchCompletion(suggestion)
}

Return to RecipesView.swift. In searchable(text:placement:prompt:), replace suggestions with the following code:

// 1
if searchQuery.isEmpty {
  if isSearchingIngredient {
    // 2
    IngredientSuggestionView()
  } else {
    // 3
    NameSuggestionView()
  }
}

Here’s what the code does:

  1. Display the suggestion list when the search field is empty.
  2. If the toggle is on, the suggestion view shows IngredientSuggestionView. You’ll create its implementation in a new file.
  3. If the toggle is off, the suggestion view shows NameSuggestionView. You’ll refactor the old implementation for the name suggestion view into a new file.

Finally, replace the else block inside filterRecipes() with the code below:

if isSearchingIngredient {
  // 1
  filteredRecipes = chefRecipesModel.recipes.filter {
    !$0.ingredients.filter { ingredient in
      ingredient.emoji == searchQuery
    }.isEmpty
  }
} else {
  // 2
  filteredRecipes = chefRecipesModel.recipes.filter {
    $0.name
      .localizedCaseInsensitiveContains(searchQuery)
  }
}

Here’s what this code does:

  1. When the user searches by ingredient, you filter the recipes by emoji inside each recipe.
  2. When the user searches by meal name, you filter the recipes by name, as before.

Build and run. Switch Toggle and help Swifty search for meals with egg in their ingredients.

Toggling between searching by ingredients with emojis and searching by meal names with text

Note: After you add the toggle, notice how the search field stays onscreen permanently according to the view hierarchy. This is because placement is automatic by default.