SwiftUI Search: Getting Started
Learn how to use the searchable modifier to quickly add search capability to your SwiftUI apps. By Mina H. Gerges.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SwiftUI Search: Getting Started
30 mins
- Getting Started
- Understanding the History of Searching in SwiftUI
- Using the Searchable Modifier
- Searching for a Meal
- Performing a Search Query
- Clearing and Canceling a Search
- Offering Search Suggestions
- Making Suggestions Dynamic
- Improving the Search Experience
- Creating Search Lists
- Setting Placement in Different Platforms
- Searching on an iPad
- Understanding Searchable Environment Properties
- Where to Go From Here?
Setting Placement in Different Platforms
searchable(text:placement:prompt:)
offers placement
to choose the location of the search bar in the view hierarchy. SwiftUI offers four options for this parameter:
- .automatic: SwiftUI places the search field automatically according to the view hierarchy. This is the default option.
-
.navigationBarDrawer: SwiftUI places the search field under the navigation title. This option has
displayMode
, which displays the search field permanently onscreen or automatically, like the previous option. - .sidebar: SwiftUI places the search field in the sidebar of a navigation view.
- .toolbar: SwiftUI places the search field in the toolbar.
The last two options provide the same output as navigationBarDrawer
. Hopefully, in future versions of SwiftUI, these options might show a significant change.
placement
is the preferred placement you choose. Sometimes the view hierarchy can’t fulfill the chosen placement.
In iOS, all those options position the search field under the navigation title. The only difference is whether the search field display is permanently onscreen or automatic.
In iPadOS and macOS, you can especially spot the difference between placement
options in multi-column views. Here are a couple of example code blocks with pictures showing how the search bar renders:
NavigationView {
FirstView()
.navigationTitle("First")
SecondView()
.navigationTitle("Second")
}
.searchable(text: $searchQuery)
In this first example, searchable(text:placement:prompt:)
is attached to a NavigationView
with two columns. Here, SwiftUI places the search field in the first column.
Here’s another example:
NavigationView {
FirstView()
.navigationTitle("First")
SecondView()
.navigationTitle("Second")
.searchable(text: $searchQuery)
}
In this code, SecondView
has searchable(text:placement:prompt:)
. Since this navigation has only two columns, SwiftUI places the search field on the top trailing of SecondView
by default.
Now, consider this example:
NavigationView {
FirstView()
.navigationTitle("First")
SecondView()
.navigationTitle("Second")
.searchable(text: $searchQuery,
placement:
.navigationBarDrawer(displayMode: .always))
}
In the code above, SecondView
has searchable(text:placement:prompt:)
, but placement
‘s value is .navigationBarDrawer
. Now, SwiftUI places the search field under the navigation title of this column.
Finally, check out this placement
option with a three-column view:
NavigationView {
FirstView()
.navigationTitle("First")
SecondView()
.navigationTitle("Second")
ThirdView()
.navigationTitle("Third")
}
.searchable(text: $searchQuery)
In the code above, NavigationView
has searchable(text:placement:prompt:)
. Here, SwiftUI places the search field under the title of the second column.
You can see that where you place searchable(text:placement:prompt:)
determines where SwiftUI draws the search box.
Now, you’ll check the placement of the search field in iPad in more depth within the Chef Secrets app. You’ll also use the second approach to filter the list using the search query.
Searching on an iPad
Choose iPad Pro 9.7″ as your simulator, then build and run. Tap the back button and check the two-column view. SwiftUI places the search field on the first column under the navigation title.
Now, you’ll implement the second approach to filter the recipe list without using .onChange(of:perform:)
and .onSubmit(of:_:)
. You’ll start with some refactoring.
First, add the lines below to the beginning of ContentView.swift:
@State var searchQuery = ""
@State var isSearchingIngredient = false
This allows ContentView
to manage the search query state.
Next, still in ContentView.swift, replace RecipesView()
with the code below:
RecipesView(
searchQuery: $searchQuery,
isSearchingIngredient: $isSearchingIngredient)
This passes @State
properties from ContentView
down to RecipesView
.
Now, open RecipesView.swift. Replace the searchQuery
and isSearchingIngredient
declarations to be @Binding
:
@Binding var searchQuery: String
@Binding var isSearchingIngredient: Bool
This allows RecipesView
to accept @State
properties from ContentView
.
Then, still in RecipesView.swift, cut the entire implementation of searchable(text:placement:prompt:)
, and paste it into ContentView.swift just below accentColor(_:)
.
Finally, open RecipesView.swift. Inside RecipesView_Previews
, replace previews
‘s content with the following code:
RecipesView(
searchQuery: .constant(""),
isSearchingIngredient: .constant(false))
.previewDevice("iPhone SE (2nd generation)")
RecipesView(
searchQuery: .constant(""),
isSearchingIngredient: .constant(false))
.previewDevice("iPad Pro (12.9-inch) (2nd generation)")
This tells the SwiftUI preview to render two versions of the view: one as an iPhone SE and the other as an iPad Pro.
Click the run icon in the canvas inside ContentView.swift to start the live preview. Check that this refactoring doesn’t break anything in the app.
The real change starts now.
First, open RecipesView.swift, then remove .onChange(of:perform:)
and .onSubmit(of:_:)
. Next, completely remove filterRecipes()
.
Finally, replace:
@State var filteredRecipes = ChefRecipesModel().recipes
With the following:
var filteredRecipes: [Recipe] {
if searchQuery.isEmpty {
return chefRecipesModel.recipes
} else {
if isSearchingIngredient {
let filteredRecipes = chefRecipesModel.recipes.filter {
!$0.ingredients.filter { ingredient in
ingredient.emoji == searchQuery
}.isEmpty
}
return filteredRecipes
} else {
return chefRecipesModel.recipes.filter {
$0.name.localizedCaseInsensitiveContains(searchQuery)
}
}
}
}
Here, you’ve changed filteredRecipes
to be a computed property and used the values of the bound variables searchQuery
and isSearchingIngredient
to update the value of the computed property as the value of the bound variables change.
As a result, while typing or when the user selects a suggestion, SwiftUI filters the recipe list.
Build and run. Try searching and switch between searching by meal name and by ingredient. Notice how SwiftUI filters the list while you type in the search field.
Wonderful! You added an amazing search experience to the Chef Secrets app. But Swifty loves dessert too. So now, it’s the time for the icing on the cake. :]
Understanding Searchable Environment Properties
SwiftUI introduces two environment variables with searchable(text:placement:prompt:)
:
- .isSearching: To check if the user is using the search field.
- .dismissSearch: To dismiss the current search process.
You’ll use .isSearching
to display how many recipes match the user’s search query.
In RecipesView.swift, add the following declaration:
@Environment(\.isSearching) var isSearching
This sets the local isSearching
variable to the value of isSearching
‘s environment variable.
Next, add these lines in body
under Toggle
:
if isSearching {
Text("""
Search Results: \(filteredRecipes.count) \
of \(chefRecipesModel.recipes.count)
""")
.foregroundColor(Color("rw-green"))
.opacity(0.5)
}
This code shows Text
with the number of filtered recipes that match the search query, but only when the user is interacting with the search field.
Build and run. Try searching, and check that Text
shows the number of filtered recipes:
searchable(text:placement:prompt:)
, isSearching
works only inside any of its sub-views but can’t work inside this parent view itself. Since ContentView.swift has searchable(text:placement:prompt:)
, then its child, RecipesView.swift, uses isSearching
, but ContentView.swift itself can’t use it.
Swifty’s now ready to cook his meal. After helping him like you did today, you might be his guest at dinner! :]