SwiftUI Search: Enhance User Experience with SwiftUI Searchable

To create applications that users will love, developers must embrace intuitive design and create a great user experience. When thinking user experience one important note is efficient data presentation. One way of achieving that is by implementing search in SwiftUI. SwiftUI offers a powerful tool for achieving this: the Searchable modifier.

In this blog post, we’ll dive into SwiftUI Searchable modifier, explore its capabilities, and learn how to search simple strings and complex objects. We will also use search suggestions and search scopes.

How to implement search in SwiftUI?

Let’s start of with a basic example where we will implement a search bar on the top of our view and then search a simple array of strings.

So we will be making a array of strings and a List where we display the strings. We will create a state variable called searchText, witch will hold the users search query and we will have a variable called searchResults that will hold the search results. 

And lastly in order to use the .searchable modifier we have to wrap our list in a NavigationStack:

import SwiftUI

struct SearchContectView: View {
    
    var arrayToSearch: [String] = ["John","Poul","Maria","Emma","Ella","James"]
    @State var searchText: String = ""
    var searchResults: [String] {
        if searchText.isEmpty {
            return arrayToSearch
        }else {
            return arrayToSearch.filter( { $0.contains(searchText)} )
        }
    }

    var body: some View {
        NavigationStack {
            List(searchResults, id: \.self) { result in
                Text(result)
            }
        }
        .searchable(text: $searchText, placement:
                .navigationBarDrawer, prompt: "Search for something")
    }
}

The result:

A little tip: When creating search it’s a good idea to convert both the search text and the text searching in to lower case. Because Ella and ella is not the same. That can be done by using .localizedCaseInsensitiveContains().

SwiftUI search list of objects

Now it’s time to make our search a bit more complex and real life like. We will create a search function where we will be filtering a array of objects.

We will create a struct called User that holds: id, name and age. We will then create a array of users and then we will create a list and make it searchable.

User model:

struct UserModel: Codable {
    let id: Int
    let name: String
    let age: Int
}

How do I filter a list in SwiftUI?

Now it’s time for us to filter our list, we will do that by using the searchable modifier and filter the array by the search term. We will make our search so that you can search for both name or age, in the same field.

We will start by implementing search by a users name:

struct SearchContectView: View {
    
    var arrayToSearch: [UserModel] = [UserModel(id: 1, name: "John", age: 10),
                                      UserModel(id: 2, name: "Poul", age: 15),
                                      UserModel(id: 3, name: "Maria", age: 20),
                                      UserModel(id: 4, name: "Emma", age: 25),
                                      UserModel(id: 5, name: "Ella", age: 30),
                                      UserModel(id: 6, name: "James", age: 35)]
    @State var searchText: String = ""
    var searchResults: [UserModel] {
        if searchText.isEmpty {
            return arrayToSearch
        }else {
            return arrayToSearch.filter( { $0.name.localizedCaseInsensitiveContains(searchText)} )
        }
    }
    var body: some View {
        NavigationStack {
            List(searchResults, id: \.id) { result in
                Text("\(result.name) - \(result.age)")
            }
        }
        .searchable(text: $searchText, placement:
                .navigationBarDrawer, prompt: "Search for something")
    }
}

The result:

Now we have added the ability to search the users array by name, now let’s make the ability to search by age.

Implement search by a users age

We will simply add an or to our searchResults filter statement and pass the searchText to a int. If the search text is a string then our int passing will return nil and therefore only search for name aka. strings.

To be exact we will add the logic to our filter modifier so it will look like this:

arrayToSearch.filter( { $0.name.localizedCaseInsensitiveContains(searchText) || 0.age == Int(searchText)}} )

struct UserModel: Codable {
    let id: Int
    let name: String
    let age: Int
}

import SwiftUI

struct SearchContectView: View {
    
    var arrayToSearch: [UserModel] = [UserModel(id: 1, name: "John", age: 10),
                                      UserModel(id: 2, name: "Poul", age: 15),
                                      UserModel(id: 3, name: "Maria", age: 20),
                                      UserModel(id: 4, name: "Emma", age: 25),
                                      UserModel(id: 5, name: "Ella", age: 30),
                                      UserModel(id: 6, name: "James", age: 35)]
    @State var searchText: String = ""
    var searchResults: [UserModel] {
        if searchText.isEmpty {
            return arrayToSearch
        }else {
            return arrayToSearch.filter( {
                $0.name.localizedCaseInsensitiveContains(searchText) ||
                $0.age == Int(searchText)
            })
        }
    }
    var body: some View {
       
        NavigationStack {
            List(searchResults, id: \.id) { result in
                Text("\(result.name) - \(result.age)")
            }
        }
        .searchable(text: $searchText, placement:
                .navigationBarDrawer, prompt: "Search for something")
    }
}

SwiftUI search suggestions

Imagine you’re building a app where the users need to pick a city by the exact name, with search suggestions you can instantly provide matching city names.

To implement search suggestions in SwiftUI we will be using the .searchSuggestions modifier and create a list of search suggestions.

var searchSuggestions: [String] = ["John","Emma","Ella","James"]

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

The result will look like this:

As you can see the search suggestions is showing quite nicely — however, when you click on a search suggestion the search results does not show, the suggestions will not disappear. 

In order to do that we will hide the search suggestions if our search text is larger then 3 characters:

.searchSuggestions {
    if searchText.count < 3 {
        ForEach(searchSuggestions, id: \.self) { suggestion in
        Text(suggestion)
            .searchCompletion(suggestion)
        }
    }
}

The complete code for search by name, age and search suggestions:

import SwiftUI

struct SearchContectView: View {
    
    var arrayToSearch: [UserModel] = [UserModel(id: 1, name: "John", age: 10),
                                      UserModel(id: 2, name: "Poul", age: 15),
                                      UserModel(id: 3, name: "Maria", age: 20),
                                      UserModel(id: 4, name: "Emma", age: 25),
                                      UserModel(id: 5, name: "Ella", age: 30),
                                      UserModel(id: 6, name: "James", age: 35)]
    @State var searchText: String = ""
    var searchResults: [UserModel] {
        if searchText.isEmpty {
            return arrayToSearch
        }else {
            return arrayToSearch.filter( {
                $0.name.localizedCaseInsensitiveContains(searchText) ||
                $0.age == Int(searchText)
            })
        }
    }
    var searchSuggestions: [String] = ["John","Emma","Ella","James"]

    var body: some View {
       
        NavigationStack {
            List(searchResults, id: \.id) { result in
                Text("\(result.name) - \(result.age)")
            }
        }
        .searchable(text: $searchText, placement:
                .navigationBarDrawer, prompt: "Search for something")
        .searchSuggestions {
            if searchText.count < 3 {
                ForEach(searchSuggestions, id: \.self) { suggestion in
                    Text(suggestion)
                        .searchCompletion(suggestion)
                }
            }
        }
    }
}

SwiftUI search scopes

Search scopes in SwiftUI is a predefined categories or filters that users can select when performing a search within a list or collection of items. These scopes allow users to narrow down their search results, making it easier to find the specific information they’re looking for. 

In our example let’s say our users is in different user groups and we want our app to be able to filter the groups in search. We will have three groups: all, normal, moderator and admin.

First we will create a user group enum that define our search scopes, create a variable that contains the selected search scope, use the .searchScopes modifier and then make the search logic.

Enum UserGroupScope:

enum UserGroupScope: String, Codable, CaseIterable {
    case all, normal, moderator, admin
}

We will set the default search scope to .all, to display all the users:

@State var searchScope = UserGroupScope.all

In the searchScopes modifier we will loop all scopes and display them in a text element:

.searchScopes($searchScope) {
    ForEach(UserGroupScope.allCases, id: \.self) { scope in
        Text(scope.rawValue)
    }
}

Now when you run the code you will see the search scope options when you start entering your search query.

Search logic for search scopes

Now we just need to use the scopes in our search logic. Firstly we need to do is to add a user role to our user model and then filter by the selected search scope before we search the users array:

struct UserModel: Codable {
    let id: Int
    let name: String
    let age: Int
    let group: UserGroupScope
}

var arrayToSearch: [UserModel] = [
UserModel(id: 1, name: "John", age: 10, group: .admin),   
UserModel(id: 2, name: "Poul", age: 15, group: .admin),
UserModel(id: 3, name: "Maria", age: 20, group: .normal),
UserModel(id: 4, name: "Emma", age: 25, group: .normal),
UserModel(id: 5, name: "Ella", age: 30, group: .moderator),
UserModel(id: 6, name: "James", age: 35, group: .normal)]

The search logic is build by checking what the searchScope is set to. If it’s .all then we will search the whole array but it it’s not set to .all, we will only search in the selected scope:

var searchResults: [UserModel] {
    if searchText.isEmpty {
        return arrayToSearch
    }else {
        var scopedResults = arrayToSearch
            
        if searchScope != .all {
            scopedResults = arrayToSearch.filter({ $0.group == searchScope })
        }
        return scopedResults.filter( {
            $0.name.localizedCaseInsensitiveContains(searchText) ||
            $0.age == Int(searchText)
        })
    }
}

The result:

The complete search logic with search scopes

struct UserModel: Codable {
    let id: Int
    let name: String
    let age: Int
    let group: UserGroupScope
}

enum UserGroupScope: String, Codable, CaseIterable {
    case all, normal, moderator, admin
}

struct SearchContectView: View {
    
    var arrayToSearch: [UserModel] = [UserModel(id: 1, name: "John", age: 10, group: .admin),
                                      UserModel(id: 2, name: "Poul", age: 15, group: .admin),
                                      UserModel(id: 3, name: "Maria", age: 20, group: .normal),
                                      UserModel(id: 4, name: "Emma", age: 25, group: .normal),
                                      UserModel(id: 5, name: "Ella", age: 30, group: .moderator),
                                      UserModel(id: 6, name: "James", age: 35, group: .normal)]
    @State var searchText: String = ""
    @State var searchScope = UserGroupScope.all
    
    var searchResults: [UserModel] {
        if searchText.isEmpty {
            return arrayToSearch
        }else {
            var scopedResults = arrayToSearch
            
            if searchScope != .all {
                scopedResults = arrayToSearch.filter({ $0.group == searchScope })
            }
            return scopedResults.filter( {
                $0.name.localizedCaseInsensitiveContains(searchText) ||
                $0.age == Int(searchText)
            })
        }
    }

    var body: some View {
       
        NavigationStack {
            List(searchResults, id: \.id) { result in
                Text("\(result.name) - \(result.age)")
            }
        }
        .searchable(text: $searchText, placement:
                .navigationBarDrawer, prompt: "Search for something")
        .searchScopes($searchScope) {
            ForEach(UserGroupScope.allCases, id: \.self) { scope in
                Text(scope.rawValue)
            }
        }
    }
}

By providing predefined filters, search scopes simplify the search process and empower users to efficiently navigate through large datasets.

Wrap up search in SwiftUI

In this blog post we have created a nice and easy search feature where we search for both a string and a int. I hope you can use it in your project and modify it so it exactly fits into your app.

Implementing search in apps is essential and a need to have feature. Implement search functionality thoughtfully:

  • Keep the search interface clean and uncluttered. A minimalist design ensures that users focus on the task at hand without distractions.
  • Provide instant feedback as users type in the search bar. This feedback reassures users that their actions are having an impact and aids in refining their search terms.
  • Optimize your search algorithms to ensure responsive results, even when dealing with substantial data sets.
Scroll to Top