SwiftUI Search Bar: Best Practices and Examples

One of the essential elements of a great app is its ability to provide a seamless user experience. That’s why including a search bar is often crucial in making an app more user-friendly. SwiftUI search bar is a great tool to help users search for content within an app quickly. However, implementing it effectively requires understanding the best practices for designing and positioning the search bar and optimizing the search algorithms to provide accurate and relevant results.

In this blog post, we’ll discuss the best practices for implementing the SwiftUI search bar and examples of apps that do it effectively. By following these best practices, you can make your app more user-friendly and help your users find the content they’re looking for quickly and easily. Let’s dive in

⬇️ Download the project files

What is a search bar in iOS?

A search bar is a user interface element that allows users to search for specific content within an app. In iOS apps, the search bar is often positioned at the top of the screen, making it easy for users to locate and use. Users can type keywords or phrases into the search bar, and the app will display results that match the search criteria. Search bars are particularly useful in apps that have a large amount of content, such as music, video, or news apps, where users may want to find specific content quickly and easily. The SwiftUI framework includes a searchable view modifier that developers can use to implement this functionality in their apps.

example for search bar on iOS
The new freeform app uses a search bar to search for drawing boards by title.

How to add a SwiftUI search bar

Creating a search bar is quite easy. As an example, I will use a food delivery app. The following code defines a Meal model:

struct Meal: Codable, Hashable, Identifiable {

    let imageURL: String
    let id: String
    let category: MealCategory
    let name: String
    let location: String
    let rating: Double
    var tags: [String]
    
    static func preview() -> Meal {
        Meal(imageURL: "europian.jpg",
             id: "3",
             category: MealCategory.oceanian,
             name: "food name",
             location: "Africa",
             rating: 4,
             tags: ["Fast Food"])
    }
}

Instead of fetching the data from a server, I will use mocked data that I load from the bundle. The following code defines the view model that sets up the data array for meals:

class MealListViewModel: ObservableObject {

    @Published var meals = [Meal]()
    private let service = DataService()
    
    init() {
        meals = service.loadMealsFromDataStorage()
    }
}

I can then use the Meal data and show it in a SwiftUI List:

import SwiftUI

struct ContentView: View {

    @StateObject var viewModel = MealListViewModel()

    var body: some View {
        NavigationStack {
            List {
                ForEach(viewModel.meals) { meal in
                   MealCardView(meal: meal)
                }
                .listRowSeparator(.hidden, edges: .all)
            }
            .listStyle(.plain)
            .navigationTitle("Find Your Next Meal")
        }
    }
}
SwiftUI example project to test search

Now we can add the search interface. First, you’ll need to create a state variable to hold the search query. I am going to add this to the view model.

class MealListViewModel: ObservableObject {
    @Published var meals = [Meal]()
    @Published var searchText: String = ""
    ...
}

Then, you can add a search bar to your app’s view hierarchy using the searchable view modifier, which takes a binding to a String type:

NavigationStack {
    List {
    ...
   }
   .searchable(text: $viewModel.searchText)
}

The list needs to be embedded inside a Navigation View, NavigationStackView, or NavigationSplitView, in order for the search text field to appear in the navigation bar.

import SwiftUI

struct ContentView: View {

    @StateObject var viewModel = MealListViewModel()

    var body: some View {
        NavigationStack {
            List {
                ForEach(viewModel.meals) { meal in
                   MealCardView(meal: meal)
                }
                .listRowSeparator(.hidden, 
                                  edges: .all)
            }
            .listStyle(.plain)
            .navigationTitle("Find Your Next Meal")
            .searchable(text: $viewModel.searchText)
        }
    }
}
swiftui list with search field

How to Display Search Results

To implement the search functionality, you’ll need to filter data with the user’s search query.

Add a computed property to compute the filtered array for meals. If the search text is empty, I don´t want to filter and instead return the full array of meals:

class MealListViewModel: ObservableObject {

    @Published var meals = [Meal]()
    @Published var searchText: String = ""
    
    var filteredMeals: [Meal] {
        guard !searchText.isEmpty else { return meals }
        return meals.filter { meal in
            meal.name.lowercased().contains(searchText.lowercased())
        }
    }
     ...
}

Now instead of the full list of meals, I am only showing the filtered data in the ForEach. The following example shows how the list displayed changes automatically when I enter a search text:.

import SwiftUI

struct ContentView: View {

    @StateObject var viewModel = MealListViewModel()

    var body: some View {
        NavigationStack {
            List {
                ForEach(viewModel.filteredMeals) { meal in
                   MealCardView(meal: meal)
                }
                .listRowSeparator(.hidden, edges: .all)
            }
            .listStyle(.plain)
            .navigationTitle("Find Your Next Meal")
            .searchable(text: $viewModel.searchText)
        }
    }
}
swiftui searchable in list

Advanced Search Features in SwiftUI

Search Suggestions

SwiftUI offers an easy way to add search suggestions to your app by using the Searchable modifiers, which provide an optional parameter called “suggestions.” You can provide a view for this parameter, which might be a few static buttons or a dynamic set of suggestions generated from your app’s database or server.

For this demo, I am using a constant property with default suggested search texts. I am not showing all suggestions, only the relevant ones that fit with the search text. Therefore I added another computed property which is ´filteredSuggestions´. The following code is a simple implementation to test search suggestions:

class MealListViewModel: ObservableObject {
    @Published var suggestions = ["Muffin", "Noodles", "Beef", "Wraps", "Hamburger", "Chicken",
                                  "Falafel", "Pita", "Avocado", "Tomato",
                                  "Chocolate", "Strawberry", "Coffee"]
    var filteredSuggestions: [String] {
        guard !searchText.isEmpty else { return [] }
        return suggestions.sorted().filter { $0.lowercased().contains(searchText.lowercased()) }
    }
    ...
}

I can then use these suggestions for the meal list view. When the user enters the search, I am not showing any suggestions. As soon as the search text is not empty, I use it to filter the relevant suggestions and show them below the search text field. When the user taps on a suggestion, the search text is updated to the selected suggestion:

.searchable(text: $viewModel.searchText, suggestions: {
    ForEach(viewModel.filteredSuggestions, id: \.self) { suggestion in
        Button {
            viewModel.searchText = suggestion
        } label: {
           Label(suggestion, systemImage: "bookmark")
        }
     }
})

Apple added a shorter form for this and replaces the button interaction with a ´searchCompletion´ modifier. The following code is equivalent to the previous implementation:

.searchable(text: $viewModel.searchText, suggestions: {
    ForEach(viewModel.filteredSuggestions, id: \.self) { suggestion in
       Text(suggestion)
         .searchCompletion(suggestion)
     }
})

Note that searchable(text:placement:prompt:suggestions:) has been soft deprecated with iOS 16.2 and macOS 13.1. You can use the newer searchSuggestions modifier instead

Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

Hidding Search Suggestions Depending on the Platform

If you want to hide search suggestions for some situations, you can use the searchSuggestions(_ visibility: Visibility, for placements:) modifier, which came with iOS 16 and macOS 13. For the placement, you have to specify SearchSuggestionsPlacement which has the cases of automatic, menu, and content. On macOS, search suggestions are displayed as a menu below the search text field, and on iOS, they are shown instead of the content. 

Example for Search suggestions for macOS and iOS
For macOS, search suggestions appear as a menu below the search text field, and on iOS, they replace the content.

This means that the following will hide suggestions on iOS:

.searchable(text: $viewModel.searchText, suggestions: {
    ForEach(viewModel.filteredSuggestions, id: \.self) { suggestion in
       Text(suggestion)
         .searchCompletion(suggestion)
     }
   .searchSuggestions(.hidden, for: .content)
})

and if you want to hide suggestions on the Mac, you can set this:

.searchSuggestions(.hidden, for: .menu)

If you for example want to show suggestions on iOS inside your main list. You can hide the suggestions on iOS only and use the environment for searchSuggestionsPlacement to check if you should show suggestions. I need to create another subview to properly get the environment value:

struct CustomSearchSuggestionView: View {

    @ObservedObject var viewModel: MealListViewModel
    @Environment(\.searchSuggestionsPlacement) var placement

    var body: some View {
        if placement == .content {
            ForEach(viewModel.filteredSuggestions, id: \.self) { suggestion in
                Button {
                    viewModel.searchText = suggestion
                } label: {
                    Label(suggestion, systemImage: "bookmark")
                }
            }
        }
    }
}
struct HideSuggestionMealListView: View {

    @StateObject var viewModel = MealListViewModel()
    @State var selectedMeal: Meal? = nil

    var body: some View {
        NavigationSplitView(sidebar: {
            List(selection: $selectedMeal) {
                CustomSearchSuggestionView(viewModel: viewModel)

                ForEach(viewModel.filteredMeals) { meal in
                    MealCardView(meal: meal)
                        .tag(meal)
                }
                .listRowSeparator(.hidden, edges: .all)
            }
            .listStyle(.plain)
            .navigationTitle("Find Your Next Meal")
            .searchable(text: $viewModel.searchText, suggestions: {
                ForEach(viewModel.filteredSuggestions, id: \.self) { suggestion in
                    Text(suggestion)
                        .searchCompletion(suggestion)
                }
               .searchSuggestions(.hidden, for: .content)
            })
        }, detail: {
            if let meal = selectedMeal {
                DetailView(meal: meal)
            } else {
                Text("Select a Meal")
            }
        })
    }
}
SwiftUI example for search with custom suggestion placement

I modified the placement of search suggestions only. For macOS, they are still displayed in the default position, which is in a menu below the search text field.

Search Tokens

A newer feature for iOS 16 and macOS 13 are search tokens. Let’s say I want to allow the user of my app to search for food that is on the go or on sale. My data has a tag property that I can use to filter, but how do I allow my users to pick these options? You might want to use a picker, but tokens are a much smoother user experience. Learn how to use search tokens in this blog post Search Tokens in SwiftUI

Creating a Scope Bar to Filter Results

You can add a scope bar to your search bar for filtering by Category. This scope control limits searches by simply searching for specific scope areas. Using scope bars can be accomplished easily by simply implementing a new searchScope modifier.

As an example, I could use the origin of the meal to limit the search scope. If you want to see the full breakdown of how I implemented search scope, check out this post about How to use Search Scopes in SwiftUI to improve search on iOS and macOS.

Other Filter Options

If you want to support filtering options, you can show a SwiftUI Picker. You can see different implementations in this post: “Master SwiftUI Picker View”. For example, you could add a picker or menu view in the toolbar. The picker options would be similar to the tokens.

Detecting and Dismissing a Search Field Programmatically

In some cases, you may want to programmatically detect when a user is interacting with the search field, or dismiss the search field when certain conditions are met. In SwiftUI, you can use the Environment value for 
isSearching and dismissSearch. You have to access this value from a view that is inside the search list. 

For example, if you want to show a different list for the search results. I am extracting my List in a reusable view like so:

struct MealListContentView: View {

    @Binding var selectedMeal: Meal?
    let meals: [Meal]

    var body: some View {
        List(selection: $selectedMeal) {
            ForEach(meals) { meal in
                MealCardView(meal: meal)
                    .tag(meal)
            }
            .listRowSeparator(.hidden, edges: .all)
            .listRowInsets(.init(top: 10, leading: 10,
                                 bottom: 10, trailing: 10))
        }
        .listStyle(.plain)
        .navigationTitle("Find Your Next Meal")

    }
}

I create a separate view to decide what view to show. If the user is in search mode, I will show an overlay with the search results. The check is done with the isSearching property and if the user entered a valid search term:

private struct MealSearchListView: View {

    @ObservedObject var viewModel: MealListViewModel
    @Environment(\.isSearching) var isSearching
    @Binding var selectedMeal: Meal?

    var body: some View {
        MealListContentView(selectedMeal: $selectedMeal,
                            meals: viewModel.meals)
            .overlay {
                if isSearching && !viewModel.searchText.isEmpty {
                    VStack {
                        Text("Search Results")
                        MealListContentView(selectedMeal: $selectedMeal,
                                            meals: viewModel.filteredMeals)
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color("background"))
                }
            }
    }
}
struct IsSearchingMealListView: View {
    @StateObject var viewModel = MealListViewModel()
    @State var selectedMeal: Meal? = nil
    var body: some View {
        NavigationStack {
            MealSearchListView(viewModel: viewModel,
                                 selectedMeal: $selectedMeal)
                .searchable(text: $viewModel.searchText)
        }
    }
}
private struct MealSearchListView: View {
    @ObservedObject var viewModel: MealListViewModel
    @Environment(\.isSearching) var isSearching
    @Binding var selectedMeal: Meal?
    var body: some View {
        MealListContentView(selectedMeal: $selectedMeal,
                            meals: viewModel.meals)
            .overlay {
                if isSearching && !viewModel.searchText.isEmpty {
                    VStack {
                        Text("Search Results")
                        MealListContentView(selectedMeal: $selectedMeal,
                                            meals: viewModel.filteredMeals)
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color("background"))
                }
            }
    }
}
swiftui is searching

In addition, you can programmatically dismiss the search. I can add a done button in the search results and use the dismissSeach Environment property:

private struct MealSearchListView: View {

    @Environment(\.isSearching) var isSearching
    @Environment(\.dismissSearch) var dismissSearch

    @ObservedObject var viewModel: MealListViewModel
    @Binding var selectedMeal: Meal?

    var body: some View {
        MealListContentView(selectedMeal: $selectedMeal,
                            meals: viewModel.meals)
            .overlay {
                if isSearching && !viewModel.searchText.isEmpty {
                    VStack {
                        HStack {
                            Text("Search Results")

                            Button("Done") {
                                dismissSearch()
                            }
                        }
                        MealListContentView(selectedMeal: $selectedMeal,
                                            meals: viewModel.filteredMeals)
                    }
                    .frame(maxWidth: .infinity, maxHeight: .infinity)
                    .background(Color("background"))
                }
            }
    }
}
Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

Performing a task when the user finishes the search

If your data comes from a server, you need to do a lot of fetching if you react to every user input. The way Apple recommends this is by using the onSubmit modifier. You can specify text or search for onSubmit. If I only wanted to execute my fetching when the user presses enter in the search text field, the following implementation would work:

NavigationStack {
    List {
        ...
    }
    .searchable(text: $viewModel.searchText)

    .onSubmit(of: .search) {
        print("fetch data from server to refresh with full search query")
        // viewModel.performQuery()
    }
}

Changing the Appearance of the Search

Per default, the text field shows the phrase “search”. This is also automatically localized in different languages. If you want to change the placeholder, you can use the prompt argument of the searchable modifier:

.searchable(text: $viewModel.searchText, prompt: "Search for Meals")

How to always show the search bar

For iOS, the search text field is not shown directly in certain situations. Only after the user pulls down the list, will it appear. 

If the search is inside the first view of a NavigationStack or the sidebar of a NavigationSplitView, the search text field is shown directly. If the seach is in the second or later view, the search is hidden initially.

You can override this behavior by using the search placement like so:

.searchable(text: $viewModel.searchText, 
            placement: .navigationBarDrawer(displayMode:.always))

Search Placement

If you have an app with multiple screens or a NavigationSplitView, things get a bit more interesting because you can add the search in different parts of your app. As an example, I am showing a 3-column NavigationSplitView, where the sidebar has a list of all the meal categories.

NavigationSplitView(columnVisibility: $columnVisibility) {
    SidebarView(selectedCategory: $viewModel.selectedCategory)
} content: {
    ContentView(meals: viewModel.filteredMeals,
                selectedMeal: $viewModel.selectedMeal)
} detail: {
    if let meal = viewModel.selectedMeal {
        DetailView(meal: meal)
    } else {
        Text("Detail")
    }
}
.searchable(text: $viewModel.searchText)

If I add a searchable modifier to the NavigationSplitView, the default placement on macOS will be used and the search text field appears in the toolbar on the trailing corner.

SwiftUI search bar placement on macOS with the toolbar placement
Per default, the search bar is added to the toolbar on macOS
SwiftUI search bar placement on macOS with the sidebar placement
You can modify the search placement on macOS by specifying the sidebar placement.

In order to change the search bar placement you can either specify a different placement like in the following:

.searchable(text: $viewModel.searchText, placement: .sidebar)

The same app on the iPhone would then add the search bar to the sidebar view.

Example for SwiftUI search in NavigationSplitView. Per default in the iPhone, the search bar is added to the sidebar area
Per default in the iPhone, the search bar is added to the sidebar area

You can change the placement of the search bar by moving the searchable view modifier to the content area:

NavigationSplitView(columnVisibility: $columnVisibility) {
    ...
} content: {
    ContentView(meals: viewModel.filteredMeals,
                selectedMeal: $viewModel.selectedMeal)
    .searchable(text: $viewModel.searchText,
                placement: .navigationBarDrawer(displayMode: .always))
} detail: {
    ...
}

I am using the display mode to always because the search UI would otherwise only be visible after the user pulls the list.

SwiftUI search field placement to the content area
You can modify the search placement on iOS to the content area.

Similarly, we can change the placement on the iPad for a 3-column layout.

Example for search bar placement on the iPad in SwiftUI
Per default on the iPad, the search field is shown in the content area.

If you want to display the search bar in the sidebar area, you have to move the searchable view modifier inside the NavigationSplitView and attach it to the sidebar view:

NavigationSplitView(columnVisibility: $columnVisibility) {
   SidebarView()
        .searchable(text: $viewModel.searchText)
} content: {
    ...
} detail: {
    ...
}
Example for search bar placement on the iPad in SwiftUI
Moving the searchable view modifier, you can modify the search bar placement to the sidebar.

Summary

Through the “SwiftUI Search Bar: Best Practices and Examples” blog post, you have learned how to add and customize a search bar in SwiftUI, including its placement, search result display, search suggestions, and programmatically dismissing the search. Additionally, you saw how to perform tasks upon completion of the search. By following the best practices outlined in the post and implementing the examples provided, you can create a more efficient and user-friendly search experience in my SwiftUI app.

⬇️ Download the project files

Further Reading:

2 thoughts on “SwiftUI Search Bar: Best Practices and Examples”

  1. I have been surfing online more than three hours today, yet I never found
    any interesting article like yours. It is pretty worth enough for me.
    In my opinion, if all website owners and bloggers made good content as you
    did, the internet will be much more useful than ever before.

    Reply
  2. This was a huge help! However, I was trying to do something that was not intuitive at first, but now makes sense. I wanted a searchable view that was part of the stack but did not specifically have “navigationStack” as part of its own view. For all the examples that you provide, you have .searchable in the same view as the navigationStack. Well, it turns out, as long as the view is part of the navigationPath, you simply add .searchable and it Works! For anyone interested: https://github.com/jrderanian/WorkoutTimeFreshStart

    Thank you for all your help Karen!

    Reply

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form