Searchable modifier in SwiftUI

  • Oct 4, 2024

Searchable modifier in SwiftUI

  • DevTechie

Unlock the power of SwiftUI with the Searchable modifier! This essential guide explores how to implement search functionality in your SwiftUI applications, enhancing user experience and making data management effortless. Discover tips, best practices, and code examples to effectively utilize the Searchable modifier, ensuring your app remains intuitive and user-friendly. Perfect for SwiftUI developers looking to elevate their projects!

Searchable modifier was introduced for iOS 15+ in SwiftUI 3. Like the name suggest, this modifier is used to add search functionality to your views and you are gonna love effortless way of adding searchbar to your apps.

Note: this modifier is only available for iOS 15 or above so you will need Xcode 13+. You will also need to make sure that your app target version is iOS 15 or above or put feature behind  #available(iOS 15.0, *).

If you prefer this article in video format check it out here šŸ‘‡:

Definition for searchable looks like this:

func searchable(text: Binding<String>, placement: SearchFieldPlacement = .automatic) -> some View

text to display and edit in the search field.

placement is preferred placement of the search field within the containing view hierarchy.

Placement supports four predefined values:

automaticThe search field is placed automatically depending on the platform.

navigationBarDrawerThe search field is placed in an drawer of the navigation bar.

sidebarThe search field is placed in the sidebar of a navigation view.

toolbarThe search field is placed in the toolbar.

Let’s put a simple example together. In this example by just adding .searchable modifier inside NavigationView will give us search bar at the top.

struct SearchableDemo: View {
    @State private var searchQuery = ""
    var body: some View {
        NavigationView {
            VStack {
                Text("Hey DevTechie!")
                Text("Find me this --> \(searchQuery)")
                    .bold()
                    .searchable(text: $searchQuery)
                    .navigationTitle("Searchable")
            }
        }
    }
}

Here is how our view will look like:

This is awesome for the amount of code we had to write but it is not very useful is it 🤨 Let’s make is more useful.

Lists are perfect candidate to have searchbar. As the list grows in size so does the need to search an item in the list so we will do just that.

Imagine that we have an app that lists courses from DevTechie(in case you didn’t notice that’s us 🤣). App’s home view is a list of courses from DevTechie but its a long list, if you are looking for something specific well in that case you are out of luck as the app doesn’t have a way to search so let’s fix that.

We will first create Course model for our app:

struct Course: Identifiable {
    let id = UUID()
    var name: String
    var author: String
    var chapters: Int
}

Course model will conform to Identifiable protocol so we will have id property initialized with UUID.

We need data to work with so let me put some of our courses into a static variable inside an extension to ourCourse struct.

extension Course {
    static var sampleCourses: [Course] {
        return [
            Course(name: "Disney Plus Clone in SwiftUI", author: "DevTechie", chapters: 15),
            Course(name: "Stocks App Clone in SwiftUI and Combine", author: "DevTechie", chapters: 19),
            Course(name: "Secure Notes app in SwiftUI", author: "DevTechie", chapters: 11),
            Course(name: "iOS 14 Widgets in SwiftUI", author: "DevTechie", chapters: 18),
            Course(name: "Top Destinations App in SwiftUI and Mapkit", author: "DevTechie", chapters: 12),
            Course(name: "Advanced Machine Learning in iOS, Swift, Core ML, Create ML", author: "DevTechie", chapters: 25),
            Course(name: "Strava Clone in iOS, UIKit, Mapkit", author: "DevTechie", chapters: 20),
            Course(name: "Pantry App in SwiftUI, Firebase, Firestore, MVVM", author: "DevTechie", chapters: 12),
            Course(name: "Writing Reusable Framework for iOS and Cocoapods", author: "DevTechie", chapters: 21),
            Course(name: "Complete Weather App in SwiftUI, MVVM, Lottie Animation", author: "DevTechie", chapters: 8),
            Course(name: "Learning HealthKit Integration in SwiftUI", author: "DevTechie", chapters: 14),
            Course(name: "SwiftUI in Depth", author: "DevTechie", chapters: 32),
            Course(name: "Data Structures and Algorithms in Swift and iOS", author: "DevTechie", chapters: 38),
            Course(name: "Complete Weather App for WatchOS using SwiftUI and Combine", author: "DevTechie", chapters: 18),
            Course(name: "PencilKit Drawing App in SwiftUi and iPadOS", author: "DevTechie", chapters: 22)
        ]
    }
}

Timeā³ to create list

struct CourseHome: View {
    var body: some View {
        NavigationView {
            List(Course.sampleCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                    
                .listRowSeparator(.hidden)
                .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }
    }
}

Now when we have our list. Let’s make search bar by adding .searchable modifier. Update CourseHome with following code:

struct CourseHome: View {
    
    @State private var searchQuery = ""
    
    var body: some View {
        NavigationView {
            List(Course.sampleCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                    
                .listRowSeparator(.hidden)
                .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }.searchable(text: $searchQuery)
    }
}

Our search bar looks awesome but it doesn’t do much. We can change that by refactoring our code a bit. One more time lets update our code:

struct CourseHome: View {
    
    @State private var searchQuery = ""
    
    var filteredCourses: [Course] {
        if searchQuery.isEmpty {
            return Course.sampleCourses
        } else {
            return Course.sampleCourses.filter { $0.name.lowercased().contains(searchQuery.lowercased()) }
        }
    }
    
    var body: some View {
        NavigationView {
            List(filteredCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                    
                .listRowSeparator(.hidden)
                .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }.searchable(text: $searchQuery)
    }
}

In the code above, we created a computed variable which will return course based on search query. If searchQuery is empty, all courses will be returned but if searchQuery is not empty, we will check if course name contains searchQuery and return the results.

Here is how our view will look like:

Search bar placement

So far we have been using searchable with default placement value which is ā€œautomaticā€ so system decide’s where the search bar can be placed. You can always change that by adding placement parameter. In our example, we will set it in navigation bar and set its display mode to .always.

struct CourseHome: View {
    
    @State private var searchQuery = ""
    
    var filteredCourses: [Course] {
        if searchQuery.isEmpty {
            return Course.sampleCourses
        } else {
            return Course.sampleCourses.filter { $0.name.lowercased().contains(searchQuery.lowercased()) }
        }
    }
    
    var body: some View {
        NavigationView {
            List(filteredCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                    
                .listRowSeparator(.hidden)
                .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }.searchable(text: $searchQuery, placement: .navigationBarDrawer(displayMode: .always))
    }
}

Placeholder/Prompt text for search bar

By default your search bar will have ā€œSearchā€ prompt text which will hint your users what to do with the text box but if you want to customize it then it can be done with prompt parameter.

struct CourseHome: View {
    
    @State private var searchQuery = ""
    
    var filteredCourses: [Course] {
        if searchQuery.isEmpty {
            return Course.sampleCourses
        } else {
            return Course.sampleCourses.filter { $0.name.lowercased().contains(searchQuery.lowercased()) }
        }
    }
    
    var body: some View {
        NavigationView {
            List(filteredCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                    
                .listRowSeparator(.hidden)
                .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }.searchable(text: $searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search DevTechie courses here")
    }
}

Search Completion

Searchable comes with a completion capability as well. This could be another list of items, maybe list of past searches that user has done or it can come from your model. We will use top 3 items from filteredCourses list to show as suggestion.

struct CourseHome: View {
    
    @State private var searchQuery = ""
    
    var filteredCourses: [Course] {
        if searchQuery.isEmpty {
            return Course.sampleCourses
        } else {
            return Course.sampleCourses.filter { $0.name.lowercased().contains(searchQuery.lowercased()) }
        }
    }
    
    var body: some View {
        NavigationView {
            List(filteredCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                    
                .listRowSeparator(.hidden)
                .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }.searchable(text: $searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search DevTechie courses here") {
            ForEach(filteredCourses.prefix(3), id: \.id) { course in
                    Text(course.name).searchCompletion(course.name)
            }
        }
    }
}

Search completion based on history

We will create an array of suggestions in the view but this can be a dynamic list fetched from user’s profile to populate their previous search keywords.

struct CourseHome: View {
    
    @State private var searchQuery = ""
    
    var filteredCourses: [Course] {
        if searchQuery.isEmpty {
            return Course.sampleCourses
        } else {
            return Course.sampleCourses.filter { $0.name.lowercased().contains(searchQuery.lowercased()) }
        }
    }
    
    var savedSearches: [String] {
        return ["SwiftUI", "Machine Learning", "Combine"]
    }
    
    var body: some View {
        NavigationView {
            List(filteredCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                
                    .listRowSeparator(.hidden)
                    .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                    .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }.searchable(text: $searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search DevTechie courses here") {
            ForEach(savedSearches, id: \.self) { search in
                Text(search).searchCompletion(search)
            }
        }
    }
}

This is awesome šŸ˜. I am getting suggestions based on my search history… SWEET.

Curb your enthusiasm mate! There is a bug here, when you try to select item from suggested items, you get the search screen with suggestions back again WHAT 🧐.

The issue here is that we are iterating over savedSearches but we never updated our searchQuery, so update our ForEach inside searchable to reflect that:

struct CourseHome: View {
    
    @State private var searchQuery = ""
    
    var filteredCourses: [Course] {
        if searchQuery.isEmpty {
            return Course.sampleCourses
        } else {
            return Course.sampleCourses.filter { $0.name.lowercased().contains(searchQuery.lowercased()) }
        }
    }
    
    var savedSearches: [String] {
        return ["SwiftUI", "Machine Learning", "Combine"]
    }
    
    var body: some View {
        NavigationView {
            List(filteredCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                
                    .listRowSeparator(.hidden)
                    .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                    .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }.searchable(text: $searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search DevTechie courses here") {
            ForEach(savedSearches.filter {$0.localizedCaseInsensitiveContains(searchQuery)}, id: \.self) { search in
                Text(search).searchCompletion(search)
            }
        }
    }
}

Search completion can be customized as well so we will add a section view inside search completion block and move Foreach inside the section as shown below in code.

struct CourseHome: View {
    
    @State private var searchQuery = ""
    
    var filteredCourses: [Course] {
        if searchQuery.isEmpty {
            return Course.sampleCourses
        } else {
            return Course.sampleCourses.filter { $0.name.lowercased().contains(searchQuery.lowercased()) }
        }
    }
    
    var savedSearches: [String] {
        return ["Swift", "SwiftUI", "Machine Learning", "Combine"]
    }
    
    var body: some View {
        NavigationView {
            List(filteredCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                
                    .listRowSeparator(.hidden)
                    .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                    .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }.searchable(text: $searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search DevTechie courses here") {
            Section(header: Text("Top Searches").bold()) {
                ForEach(savedSearches.filter {$0.localizedCaseInsensitiveContains(searchQuery)}, id: \.self) { search in
                    Text(search).searchCompletion(search)
                }
            }
        }
    }
}

We don’t have to use Text view as well. Let’s try to replace that with a Label view.

struct CourseHome: View {
    
    @State private var searchQuery = ""
    
    var filteredCourses: [Course] {
        if searchQuery.isEmpty {
            return Course.sampleCourses
        } else {
            return Course.sampleCourses.filter { $0.name.lowercased().contains(searchQuery.lowercased()) }
        }
    }
    
    var savedSearches: [String] {
        return ["Swift", "SwiftUI", "Machine Learning", "Combine"]
    }
    
    var body: some View {
        NavigationView {
            List(filteredCourses) { course in
                VStack(alignment: .leading, spacing: 20) {
                    Text(course.name)
                        .bold()
                        .font(.title3)
                    
                    HStack {
                        Text("Chapters: \(course.chapters)")
                        Spacer()
                        Text("By \(course.author)")
                    }
                }.padding()
                
                    .listRowSeparator(.hidden)
                    .background(RoundedRectangle(cornerRadius: 10).fill(Color.blue))
                    .foregroundColor(.white)
            }
            .listStyle(PlainListStyle())
            .navigationTitle("DevTechie Courses")
        }.searchable(text: $searchQuery, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search DevTechie courses here") {
            Section(header: Text("Top Searches").bold()) {
                ForEach(savedSearches.filter {$0.localizedCaseInsensitiveContains(searchQuery)}, id: \.self) { search in
                    Label {
                        Text(search)
                    } icon: {
                        Image(systemName: "magnifyingglass")
                    }
                    .searchCompletion(search)
                }
            }
        }
    }
}

With that, we have reached the end of this article. Thank you once again for reading.