How to create a Search Bar with SwiftUI

Share on facebook
Share on twitter
Share on pinterest

Hello and welcome to a new SwiftUI tutorial. Today we will learn how to build a fully functional search bar for our SwiftUI apps. We won’t use the UIKit framework for this but build our search bar using native SwiftUI views only.

This is how our finished search bar will look like:

Unfortunately, SwiftUI does not (yet) offer us a native search bar that we can use for our apps. However, this should not prevent us from using them in our SwiftUI apps. With a few tricks we can easily create and customise our own SwiftUI search bar.

Some preparations 💻

Let’s start with creating a new Xcode project and naming it “MyFruits”.

Before we implement our search bar, we need a corresponding list that contains all possible search results. For this purpose, we use the pre-generated ContentView

We put all possible matches into an array, which we initialize inside our ContentView.

struct ContentView: View {
     
     let myFruits = [
         "Apple 🍏", "Banana 🍌", "Blueberry 🫐", "Strawberry 🍓", "Avocado 🥑", "Cherries 🍒", "Mango 🥭", "Watermelon 🍉", "Grapes 🍇", "Lemon 🍋"
     ]
     
     var body: some View {
         Text("Hello, world!")
             .padding()
     }
 }

Now we replace the pre-generated Text view with a List. We feed this List with a ForEach loop that produces one Text view for each String in our myFruits array.

var body: some View {
     List {
         ForEach(myFruits, id: \.self) { fruit in
                 Text(fruit)
             }
     }
         .listStyle(GroupedListStyle())
 }

We also add a navigation bar to our ContentView by wrapping the List in a NavigationView and using the .navigationTitle modifier.

NavigationView {
              List {
                  ForEach(myFruits, id: .self) { fruit in
                      Text(fruit)
                  }
              }
                  .listStyle(GroupedListStyle())
                  .navigationTitle("MyFruits")
          }

That’s it. Our finished ContentView should now look like this:

Creating a custom Search Bar view 🔦

Let’s start by building the interface of our search bar. 

In the end, we want our search bar to look like this:

The background of our tab bar should be a lighter/darker Gray in light/dark mode. To do this, we quickly create a custom color set in our Assets folder and name it “LightGray”.

Back to our ContentView: We want to place our search bar right above the List. Therefore, wrap the List into a VStack like this:

VStack {
     List(myFruits, id: \.self) { fruit in
         ForEach(myFruits, id: \.self) { fruit in
             Text(fruit)
         }
     }
         .listStyle(GroupedListStyle())
         .navigationTitle("MyFruits")
 }

Basically, our search bar should simply consist of a gray background on which we then place an Image view for the “magnifying glass” icon and a TextField. 

For this purpose, we place a ZStack into the VStack. Let’s choose the .leading alignment mode and apply a .frame to it. For the background, we simply use a gray Rectangle.

VStack(alignment: .leading) {
     ZStack {
         Rectangle()
             .foregroundColor(Color("LightGray"))
     }
         .frame(height: 40)
         //...
 }

We now place an HStack on top of the Rectangle. This HStack consists of an Image view with the “magnifyingglass” icon from the SFSymbols app. 

ZStack {
     HStack {
         Image(systemName: "magnifyingglass")
     }
     Rectangle()
         .foregroundColor(.gray)
 }

Next to it, we want to place our TextField. Accordingly, we require a property for our ContentView to be bound to the TextField.

@State var searchText = ""

Now we can initialize our TextField as usual. We also use a .padding to increase the distance between the Image and the TextField and change their .foregroundColor.

HStack {
     Image(systemName: "magnifyingglass")
     TextField("Search ..", text: $searchText)
 }
     .foregroundColor(.gray)
     .padding(.leading, 13)

Finally, we round off the corners of the entire ZStack and add some .padding to all sides again.

ZStack {
     //...
 }
     .frame(height: 40)
     .cornerRadius(13)
     .padding()

And this is what our finished search bar looks like:

Let’s outsource the search bar by CMD-clicking on our ZStack and selecting “Extract subview”. Let’s name the extracted subview “SearchBar”

struct SearchBar: View {
     
     var body: some View {
         ZStack {
             Rectangle()
                 .foregroundColor(Color("LightGray"))
             HStack {
                 Image(systemName: "magnifyingglass")
                 TextField("Search ..", text: $searchText)
             }
             .foregroundColor(.gray)
             .padding(.leading, 13)
         }
             .frame(height: 40)
             .cornerRadius(13)
             .padding()
     }
 }

Let’s add the corresponding searchText Binding to the extracted SearchBar

struct SearchBar: View {
     
     @Binding var searchText: String
     
     var body: some View {
         //...
     }
 }

… and initialise it from the ContentView.

VStack(alignment: .leading) {
     SearchBar(searchText: $searchText)
     //...
 }

Changing our interface while searching 👨‍💻

Once the user taps on the TextField to start searching, we want to change the navigation bar title. We also want to provide a “Cancel” button while searching.

To do this, we need to be aware of when the user starts the search. For this purpose, we add a corresponding State to our ContentView.

@State var searching = false

We pass this on to our SearchBar as a Binding.

@Binding var searching: Bool

Again, we initialize this accordingly in our ContentView.

SearchBar(searchText: $searchText, searching: $searching)

The user starts its search process by tapping the TextField in the SearchBar. We can easily detect this by adding the “onEditingChanged” closure to the TextField. This will be executed as soon as the user starts editing the TextField. As soon as this happens, we change the searching property to true.

TextField("Search ..", text: $searchText) { startedEditing in
     if startedEditing {
         withAnimation {
             searching = true
         }
     }
 }

Consequently, we want to set searching to false as soon as the user taps the return key of the keyboard. To do this, we append the “onCommit” closure to our TextField:



 TextField("Search ..", text: $searchText) { startedEditing in
     if startedEditing {
         withAnimation {
             searching = true
         }
     }
 } onCommit: {
     withAnimation {
         searching = false
     }
 }

Back to our ContentView. Once the search has started, we want to change the existing .navigationTitle

.navigationTitle(searching ? "searching" : "MyFruits")

We also like to provide a “Cancel” button during the search process. For this, we use the .toolbar modifier. 

VStack(alignment: .leading) {
     //...
         .navigationTitle(searching ? "Searching" : "MyFruits")
         .toolbar {
             if searching {
                 Button("Cancel") {
                     searchText = ""
                     withAnimation {
                         searching = false
                     }
                 }
             }
         }
 }
 

Let’s see if this works:

As you can see, the keyboard disappears when we tap on the return key (because then the “onCommit” closure of our TextField gets executed). However, the keyboard doesn’t disappear when we tap on the “Cancel” Button in our NavigationBar. 

Resigning the keyboard ⌨️

To fix this, we need to find a way to hide the keyboard manually. There is no native SwiftUI feature for this, so again we have to rely on the UIKit.

Just add the following extension to your ContentView.swift file.

extension UIApplication {
      func dismissKeyboard() {
          sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
      }
  }
 

The code is quite complicated. Basically, it says that the “control” that currently commands the keyboard should stop commanding it. See this post if you are interested in the topic further.

We can now add the following line to our “Cancel” Button.

Button("Cancel") {
     searchText = ""
     withAnimation {
         searching = false
         UIApplication.shared.dismissKeyboard()
     }
 }

If we now tap on the “Cancel” Button, the keyboard disappears!

To provide a better user experience, we also want to hide the keyboard while the user is scrolling through the search results.

To do this, we add a .gesture to the List in our ContentView. There are many different gestures available in SwiftUI, for example, the TapGesture or the onLongPressGesture. Here we use a DragGesture.

List(myFruits, id: \.self) { fruit in
     //...
 }
     //...
     .gesture(DragGesture()
          
     )

Once the user performs a drag gesture across the List, we want to dismiss the keyboard. For this purpose, we use the .onChanged modifier:

.gesture(DragGesture()
             .onChanged({ _ in
                 UIApplication.shared.dismissKeyboard()
             })
 )

When we run our app now, the keyboard will also be hidden when swiping over the list while performing a search.

Filtering our List’s results 🕵️‍♀️

Okay, we’re almost there. All we need to do now is implement the actual search functionality. 

While the user is searching we want to display the search results instead of all fruits available. For this, we modify our ForEach loop to only display those Strings in our myFruits array that match the searchText.

List {
     ForEach(myFruits.filter({ (fruit: String) -> Bool in
         return fruit.hasPrefix(searchText) || searchText == ""
     }), id: \.self) { fruit in
         Text(fruit)
     }
 }

Using the filter function we cycle through String in our myFruits array. Only if the particular String has the same initial letters as the searchText (or if there is no searchText at all), we use it for our ForEach loop.

We can now run our app and see if our search bar works!

Conclusion 🎊

Awesome! You have just learned how to build your own search bar using SwiftUI views only. We hope you have fun applying what you’ve learned and implementing your own custom search bars for your SwiftUI apps.

We’ve uploaded the source code for the app to GitHub.

If you want to learn to combine a search bar with a fully-functional To-Do app check out the corresponding chapter in our Interactive Mastering SwiftUI Book. In this you’ll also learn how to build more awesome apps such as a chat messenger and a photo editor with SwiftUI!

Leave a Reply

Your email address will not be published. Required fields are marked *

small_c_popup.png

Covid-19 Forces you into quarantine?

Start Mastering swiftUI Today save your 33% discount