Creating a REUSABLE search bar in SwiftUI

Overview

Having the ability to search and retrieve information that you asked for is almost an implied feature in today’s applications. Users want the ability to be able to search for the content that they want and have it delivered to them in a heartbeat. Whether they’re searching for food on a grocery store app, or they’re looking for running shoes on Nike, nearly all searching scenarios are fundamentally the same and are very common.

Although search bars are building blocks for apps in the 21st century, SwiftUI does not yet have a native component for them. So, as a developer, you’re stuck in a scenario where you can download some pod to get the search functionality for your app, extend UIKit into SwiftUI, OR you can learn to make a generic search bar yourself that you can reuse in all your apps!

You’re already this far into the blog so you might as well stick around and learn how to build it yourself! Still here? Well, it sounds like you chose to SwiftUI search bar from scratch πŸ•΅οΈ. Let’s get into it!


Basic UI for a search Bar

Before we can learn to make a generic search bar that can be used for any app, let’s first create a search bar that can work for a simple app that allows you to search for grocery items.

We’ll start by creating a protocol to define a grocery store item:

protocol GroceryItem {
    var name: String { get }
    var aisle: Int { get }
}

Next, we’ll create a search-bar by utilizing a TextField for the input of the search criteria, along with a List to display the items that match the search criteria. A very generic (kind of punny) implementation of this can be seen below. Notice that the Item‘s conform to the GroceryItem which means all item’s in the array have a name and an aisle property which we can then use to filter the items!

struct GrocerySearchBar<Item, ListElement>: View where Item: Hashable & GroceryItem, ListElement: View {
    // items that can be searched
    let items: [Item]
    
    // string for users search input
    @State var textFieldString = ""
    
    var body: some View {
        VStack {
            // search box
            HStack {
                Image(systemName: "magnifyingglass")
                TextField("Search for grocery items", text: $textFieldString)
            }
            .font(.system(size: 14))
            .foregroundColor(.gray)
            .padding([.vertical, .horizontal], 8)
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(Color.gray, lineWidth: 1)
            )
            .padding(.horizontal, 16)
            .padding(.vertical, 8)
            
            // list of grocery items
            List {
                ForEach(items.filter { showItem($0) }, id: \.self) { item in
                    Text(item.name)
                }
            }
        }
    }
}

// extension
extension GrocerySearchBar {
    private func showItem(_ item: GroceryItem) -> Bool {
        guard !textFieldString.isEmpty else { return true }
        return item.name.lowercased().contains(textFieldString.lowercased())
    }
}

If you have an eagle eye, you can see that we use some functional programming to filter out items that do not meet the search criteria. By doing this, we never have to mutate the original array that is passed in. We simply pass each item in question into the showItem function and then allow the filter function to discard any items that do not meet the search criteria!

This implementation is great for a simple grocery store app, but what if we wanted to use it to search for baseball players in the MLB?

Our current implementation of the search-bar wouldn’t work because we assume every item that gets passed into the search-bar implements the GroceryItem protocol. Last time I checked, Bryce Harper doesn’t have an aisle! (or maybe he does and it’s the aisle where he’s cleaning up the champagne from the Nats world series πŸ€·β€β™‚οΈ) Enough shade haha. We can obviously see that this current implementation is NOT SCALABLE to data types outside of GroceryItem's. The current implementation filters based on the name property. How can we go about creating a search on a generic property?


Making the search bar generic

To make our search-bar generic, we need to define what properties we expect a searchable item to have. To do this, we will create a Searchable protocol.

/// Protocol allowing datatypes to be searched for
protocol Searchable {
    /// string to be searched by
    var searchString: String { get }
}

The Searchable protocol will require anything that implements Searchable to have a property called searchString. searchString will give our search-bar a property that will allow us to filter on while giving the developer the power to define the way they want each item to be searched by. Maybe it’s a name, a date, color, etc. It’s up to the dev to decide how that datatype will be searched!

Next, we will update the search bar to take an array of some type that conforms to the Searchable protcol. We can now filter the array based on the searchString property, since we know all items in the array have it!

Along with the generic searchablitiy, we also want the ability to have a generic view displayed in the results list. To do this, we add a closure which takes an item and expects a ListElement in return (which is an alias for a View). We then call this closure for each item that is being displayed in the results list. By doing this, we can display complex elements that meet the needs of any app!

struct SearchBar<T, ListElement>: View where T: Searchable & Hashable, ListElement: View {
    // items that can be searched
    let items: [T]
    
    // view to display as element for each item
    let itemDetailView: (T) -> ListElement
    
    // customizable default search string
    var defaultSearchString = "Search for items..."
    
    // string for users search input
    @State var textFieldString = ""
    
    var body: some View {
        VStack {
            // search bar input field
            HStack {
                Image(systemName: "magnifyingglass")
                TextField(defaultSearchString, text: $textFieldString)
                    .font(.system(size: 14))
            }
            .foregroundColor(.gray)
            .padding([.vertical, .horizontal], 8)
            .overlay(
                RoundedRectangle(cornerRadius: 8)
                    .stroke(Color.gray, lineWidth: 1)
            )
            .padding(.horizontal, 16)
            .padding(.vertical, 8)
            
            // list of resulting items that meet the search criteria
            List {
                ForEach(items.filter { showItem($0) }, id: \.self) { item in
                    self.itemDetailView(item)
                }
            }
        }
    }
}

extension SearchBar {
    /// show item if there is no search text or
    /// if the search criteria is contained in the searchString
    private func showItem(_ item: Searchable) -> Bool {
        guard !textFieldString.isEmpty else { return true }
        return item.searchString.lowercased().contains(textFieldString.lowercased())
    }
}

πŸ₯‚πŸ₯‚ You now have a fully generic search-bar πŸ₯‚πŸ₯‚


Future Improvements

Although this simple search bar is great for most applications, you can always add improvements. Some attributes to think adding to the search-bar are:

  • ability to change the search font/color
  • ability to change the color of the search bar rectangle
  • ability to change the image in the search bar
  • Add a clear button to the search bar

I’m sure there are plenty of other improvements, but those are just a few that I can think of. I’m going to leave that as a homework assignment for you. If you get stuck, try to model it after how I created the defaultSearchString. Notice that you can add your own value for the default search string when you create the view. Think about how that functionality can be used for the other improvements!


Wrap Up

Thanks for taking the time to read this weeks blog on SwiftUI! I hoped you enjoyed learning how to use protocol driven programming to create an awesome generic search-bar that can be utilized in so many of your apps!

If you enjoyed this content, add your email to the mailing list ⬇️ down below ⬇️ so you don’t miss any of the awesome upcoming content! Also, if you want to support my coffee addiction, I would really appreciate it if you donated a coffeeβ˜•. I think there is a direct correlation between my coffee intake and the number of blogs put out each week (just saying haha).

Have a great rest of your weekend!

Best,

Brady


Shoutout to Markus for the awesome photo. Find more of his stuff here -> Photo by Markus Winkler on Unsplash

Leave A Comment