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