SwiftUI Search: How to Add a Native Search Field to Your App

·

12 min read

Search is a common feature many apps need to provide a better user experience. With SwiftUI, you can easily add a search field to your app using the searchable modifier. In this article, you will learn how to use this modifier to filter data, display suggestions, and customize the appearance of the search field.

Searching the Unknown - Bing Image Creator

Introduction

SwiftUI is a declarative framework that lets you build user interfaces for iOS, iPadOS, macOS, watchOS, and tvOS. SwiftUI provides many built-in views and modifiers that you can use to create beautiful and responsive UIs with less code.

One of the new features that SwiftUI introduced in iOS 15 and macOS 12 is the searchable modifier. This modifier allows you to add a native search field to your app that integrates seamlessly with your SwiftUI views. You can use this modifier to perform search queries on your data model and update your UI accordingly.

In this article, you will learn how to use the searchable modifier to implement search in your SwiftUI app. You will also learn how to:

  • Filter data using the search query

  • Display suggestions for text and tokens

  • Customize the placement and appearance of the search field

To follow along, you will need Xcode 13 or later and a device or simulator running iOS 15 or macOS 12.

Filter data using the search query

The searchable modifier takes a binding to a string that represents the search query. This string is updated whenever the user types or deletes text in the search field. You can use this string to filter your data model and display only the matching results.

For example, suppose you have an app that displays a list of posts from an API. You want to allow the user to search for posts by title or author. To do this, you can create a struct that conforms to the View protocol and add a @State property to store the search query:

import SwiftUI

struct PostListView: View {
    @State private var searchQuery = ""

    var body: some View {
        // ...
    }
}

Then, you can apply the searchable modifier to your view and pass the $searchQuery binding:

import SwiftUI

struct PostListView: View {
    @State private var searchQuery = ""

    var body: some View {
        NavigationView {
            List {
                // ...
            }
            .searchable(text: $searchQuery)
            .navigationTitle("Posts")
        }
    }
}

This will display a magnifying glass icon in the navigation bar that opens the search field when tapped. You can also swipe down on the list to reveal the search field.

Next, you need to filter your data model based on the search query. To do this, you can create a computed property that returns an array of posts that match the query:

import SwiftUI

struct PostListView: View {
    @State private var searchQuery = ""
    @State private var posts: [Post] = []

    // A computed property that returns an array of filtered posts
    var filteredPosts: [Post] {
        // If the query is empty, return all posts
        guard !searchQuery.isEmpty else {
            return posts
        }

        // Lowercase the query to make it case-insensitive
        let lowercasedQuery = searchQuery.lowercased()

        // Return only the posts that match the query by title or author
        return posts.filter { post in
            post.title.lowercased().contains(lowercasedQuery) ||
            post.author.lowercased().contains(lowercasedQuery)
        }
    }

    var body: some View {
        NavigationView {
            List {
                // ...
            }
            .searchable(text: $searchQuery)
            .navigationTitle("Posts")
        }
    }
}

Finally, you need to display only the filtered posts in your list view. To do this, you can iterate over the filteredPosts array instead of the posts array:

import SwiftUI

struct PostListView: View {
    @State private var searchQuery = ""
    @State private var posts: [Post] = []

    var filteredPosts: [Post] {
        // ...
    }

    var body: some View {
        NavigationView {
            List(filteredPosts) { post in // Iterate over filteredPosts instead of posts
                VStack(alignment: .leading) {
                    Text(post.title)
                        .font(.headline)
                    Text(post.author)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }

To filter the posts based on the search query, you can use a computed property that returns an array of posts that match the query by title or author:

import SwiftUI

struct PostListView: View {
    @State private var searchQuery = ""
    @State private var posts: [Post] = []
    // A computed property that returns an array of filtered posts
    var filteredPosts: [Post] {
        // If the query is empty, return all posts
        guard !searchQuery.isEmpty else {
            return posts
        }
        // Lowercase the query to make it case-insensitive
        let lowercasedQuery = searchQuery.lowercased()
        // Return only the posts that match the query by title or author
        return posts.filter { post in
            post.title.lowercased().contains(lowercasedQuery) ||
            post.author.lowercased().contains(lowercasedQuery)
        }
    }
    var body: some View {
        NavigationView {
            List(filteredPosts) { post in // Iterate over filteredPosts instead of posts
                VStack(alignment: .leading) {
                    Text(post.title)
                        .font(.headline)
                    Text(post.author)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }
            .searchable(text: $searchQuery)
            .navigationTitle("Posts")
        }
    }
}

This way, you can display only the posts that match the user’s input in the search field. This will display a magnifying glass icon in the navigation bar that opens the search field when tapped. You can also swipe down on the list to reveal the search field.

Display suggestions for text and tokens

Sometimes, you may want to provide some suggestions for the user to improve their search experience. For example, you may want to show some popular or recent keywords that are relevant to your app’s content. SwiftUI allows you to display suggestions for both text and tokens using the searchSuggestions modifier.

Text suggestions are simple strings that appear below the search field as the user types. The user can tap on a text suggestion to fill the search field with it. You can use text suggestions to offer some common or frequent queries that your app supports.

To display text suggestions, you need to apply the searchSuggestions modifier to your view and pass a closure that returns a view containing the suggestions. The closure receives an argument called isSearching that indicates whether the user is currently interacting with the search field. You can use this argument to conditionally show or hide your suggestions.

For example, suppose you have an array of strings that represent some popular keywords for your app’s content. You can display them as text suggestions using a ForEach loop inside a VStack:

import SwiftUI

struct PostListView: View {
    @State private var searchQuery = ""
    @State private var posts: [Post] = []
    // An array of popular keywords
    let popularKeywords = ["SwiftUI", "iOS", "macOS", "Swift"]
    var body: some View {
        NavigationView {
            List {
                // ...
            }
            .searchable(text: $searchQuery)
            .searchSuggestions { isSearching in // Add a closure that returns a view with suggestions
                if isSearching { // Only show suggestions when searching
                    VStack(alignment: .leading) {
                        Text("Popular Keywords")
                            .font(.caption)
                            .foregroundColor(.secondary)
                        ForEach(popularKeywords, id: \.self) { keyword in // Iterate over popularKeywords
                            Text(keyword) // Display each keyword as a text suggestion
                                .padding(.vertical, 4)
                                .onTapGesture {
                                    searchQuery = keyword // Fill the search field with the tapped keyword
                                }
                        }
                    }
                    .padding()
                }
            }
            .navigationTitle("Posts")
        }
    }
}

This will display a list of popular keywords below the search field when the user starts typing. The user can tap on any keyword to fill the search field with it.

Tokens are another type of suggestion that you can display in SwiftUI. Tokens are discrete pieces of information that represent a specific attribute or value for your app’s content. Tokens appear as rounded rectangles inside the search field and can be removed by tapping on them. You can use tokens to offer some filters or categories that your app supports.

To display tokens, you need to apply the searchable modifier with an additional binding to an array of tokens. A token is any type that conforms to both Hashable and SearchableToken protocols. The SearchableToken protocol requires you to implement two properties: a localizedStringKey that represents the token’s label and a systemImage that represents the token’s icon. You can use any type that conforms to these protocols, such as an enum or a struct.

For example, suppose you have an enum that represents some categories for your app’s content. You can make it conform to SearchableToken by adding the required properties:

import SwiftUI
enum Category: String, CaseIterable, SearchableToken {
    case swiftUI = "SwiftUI"
    case iOS = "iOS"
    case macOS = "macOS"
    case swift = "Swift"
    // The label of the token
    var localizedStringKey: LocalizedStringKey {
        rawValue
    }
    // The icon of the token
    var systemImage: String {
        switch self {
        case .swiftUI:
            return "square.and.pencil"
        case .iOS:
            return "iphone"
        case .macOS:
            return "desktopcomputer"
        case .swift:
            return "swift"
        }
    }
}

Then, you can create a @State property to store an array of selected tokens:

import SwiftUI
struct PostListView: View {
    @State private var searchQuery = ""
    @State private var posts: [Post] = []
    @State private var selectedTokens: [Category] = [] // An array of selected tokens
    var body: some View {
        // ...
    }
}

Next, you can apply the searchable modifier with both $searchQuery and $selectedTokens bindings:

import SwiftUI
struct PostListView: View {
    @State private var searchQuery = ""
    @State private var posts: [Post] = []
    @State private var selectedTokens: [Category] = []
    var body: some View {
        NavigationView {
            List {
                // ...
            }
            .searchable(text: $searchQuery, tokens: $selectedTokens) // Add tokens binding
            .navigationTitle("Posts")
        }
    }
}

This will display a list of tokens below the search field when the user starts typing. The user can tap on any token to add it to the search field. The user can also remove any token by tapping on it.

To filter the posts based on the selected tokens, you can use another computed property that returns an array of posts that match the query and the tokens by category:

import SwiftUI
struct PostListView: View {
    @State private var searchQuery = ""
    @State private var posts: [Post] = []
    @State private var selectedTokens: [Category] = []
    // A computed property that returns an array of filtered posts by query and tokens
    var filteredPostsByQueryAndTokens: [Post] {
        // If both the query and the tokens are empty, return all posts
        guard !searchQuery.isEmpty || !selectedTokens.isEmpty else {
            return posts
        }
        // Lowercase the query to make it case-insensitive
        let lowercasedQuery = searchQuery.lowercased()
        // Return only the posts that match the query and the tokens by category
        return posts.filter { post in
            let matchesQuery = post.title.lowercased().contains(lowercasedQuery) ||
                post.author.lowercased().contains(lowercasedQuery)
            let matchesTokens = selectedTokens.contains(post.category)
            return matchesQuery && matchesTokens
        }
    }
    var body: some View {
        NavigationView {
            List(filteredPostsByQueryAndTokens) { post in // Iterate over filteredPostsByQueryAndTokens instead of filteredPosts
                VStack(alignment: .leading) {
                    Text(post.title)
                        .font(.headline)
                    Text(post.author)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }
            .searchable(text: $searchQuery, tokens: $selectedTokens)
            .navigationTitle("Posts")
        }
    }
}

This way, you can display only the posts that match both the user’s input and the selected tokens in the search field.

Customize the placement and appearance of the search field

By default, SwiftUI places the search field in a sensible location depending on your view hierarchy and platform. However, you may want to customize where and how the search field appears in your app. SwiftUI provides some options for you to do that using the placement parameter of the searchable modifier and some environment values.

The placement parameter of the searchable modifier allows you to suggest a SearchFieldPlacement value for your search field. This value indicates where you want SwiftUI to place your search field. The possible values are:

  • .automatic: SwiftUI places the search field in a sensible location depending on your view hierarchy and platform. This is the default value.

  • .navigationBar: SwiftUI places the search field in the navigation bar. This value is only available on iOS and iPadOS.

  • .sidebar: SwiftUI places the search field in the sidebar. This value is only available on macOS.

For example, if you want to place the search field in the navigation bar on iOS and iPadOS, you can use the .navigationBar value:

import SwiftUI
struct PostListView: View {
    @State private var searchQuery = ""
    @State private var posts: [Post] = []
    var body: some View {
        NavigationView {
            List {
                // ...
            }
            .searchable(text: $searchQuery, placement: .navigationBar) // Suggest navigationBar placement
            .navigationTitle("Posts")
        }
    }
}

This will display the search field in the navigation bar instead of below it.

To customize the appearance of the search field, you can use some environment values that SwiftUI provides. These values allow you to control some aspects of the search field, such as its visibility, its prompt text, and its cancel button.

To use environment values, you need to apply the environment modifier to your view and pass a key path to an environment value and a value for it. For example, if you want to change the prompt text of the search field, you can use the .searchFieldPrompt environment value:

import SwiftUI
struct PostListView: View {
    @State private var searchQuery = ""
    @State private var posts: [Post] = []
    var body: some View {
        NavigationView {
            List {
                // ...
            }
            .searchable(text: $searchQuery)
            .environment(\.searchFieldPrompt, "Search by title or author") // Change prompt text
            .navigationTitle("Posts")
        }
    }
}

This will display “Search by title or author” as the placeholder text in the search field instead of “Search”.

Some of the environment values that you can use to customize the search field are:

  • .isSearchEnabled: A Boolean value that indicates whether searching is enabled for this view. You can set this value to false to disable searching temporarily.

  • .isSearching: A Boolean value that indicates whether the user is currently interacting with a search field that has been placed by a surrounding searchable modifier. You can use this value to conditionally show or hide other views based on the search state.

  • .searchFieldPrompt: A string that represents a prompt for a search field that has been placed by a surrounding searchable modifier. You can use this value to customize the placeholder text of the search field.

  • .dismissSearch: A closure that dismisses a search field that has been placed by a surrounding searchable modifier. You can use this value to programmatically dismiss the search field when needed.

Conclusion

In this article, you learned how to use the searchable modifier to add a native search field to your SwiftUI app. You also learned how to filter data using the search query, display suggestions for text and tokens, and customize the placement and appearance of the search field.

Search is a powerful feature that can enhance your app’s usability and functionality. With SwiftUI, you can easily implement search in your app using declarative and concise code.

If you want to learn more about SwiftUI and how to build amazing apps with it, you can check out these resources:

I hope this article has helped you learn more about how to use searchable modifier in SwiftUI. If you have any questions or feedback, please let me know.😊