How to make a TextView look like a system search bar

Written by
Rob Bentley

Originally Published on February 6, 2023
Republished on June 22, 2023

I was recently creating an app where I had to perform a search with a server and was starting to create the search bar. The first thing I found when googling "SwiftUI search bar" was to add a ".searchable" modifier in a NavigationStack, like this:

NavigationStack 
    Text("Searching for \(searchText)")
        .navigationTitle("Searchable Example")
    }
    .searchable(text: $searchText) 

This didn’t exactly fit my use case, so I had to dig deeper.

I traditionally start with a ZStack and then create my views from there. I also wanted to get the functionality of a TextView, but make it look like a system search bar. So, I needed a custom view for this.

Here it is if anyone wants to copy it!

import SwiftUI

struct ContentView: View {
    @State private var searchText = ""
    @State private var isEditingSearchBar = false
    @State var resultsAreEmpty = false

    var body: some View {
        ZStack {
            Color.white
            VStack {
                searchBarView
                Spacer()
            }
            VStack(alignment:.center) {
                Spacer()
                Text("Search Text is: \(searchText)")
                Spacer()
            }
        }
    }
    var searchBarView: some View {
        HStack {
            ZStack {
                RoundedRectangle(cornerRadius: 5)
                    .stroke(lineWidth: 1.0)
                    .background(Color.init(uiColor: UIColor.white))
                    .frame(height: 40)
                    .clipped()
                    .cornerRadius(5)
                TextField("Search ...", text: $searchText, onEditingChanged: {
                    changed in
                    if changed {
                        isEditingSearchBar = true
                    }
                })
                .accentColor(.blue)
                .frame(height: 40)
                .foregroundColor(.black)
                .padding(EdgeInsets(top: 0, leading: 25, bottom: 0, trailing: 0))
                .cornerRadius(5)
                .placeholder(when: searchText.isEmpty) {
                    Text("Search...").foregroundColor(Color.init(uiColor: UIColor.lightGray))
                        .padding(.leading, 25)
                }
                .overlay(
                    HStack {
                        Image(systemName: "magnifyingglass")
                            .foregroundColor(.gray)
                            .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
                            .padding(.leading, 0)
                        if isEditingSearchBar {
                            Button(action: {
                                self.searchText = ""
                                resultsAreEmpty = false
                            }) {
                            Image(systemName: "multiply.circle.fill")
                                 .foregroundColor(.gray)
                                 .padding(.trailing, 6)
                            }
                        }
                    }
                )
                .padding(.horizontal, 10)
                .onChange(of: searchText) { text in
                    print("onChange text: \(text)")
                    resultsAreEmpty = false
                    if text.count < 1 {
                        // Remove Loading View
                        // Remove Results View
                        // Remove Error View
                        print("\(String(describing: type(of: self))), \(#function), \(#line)")
                    } else {
                        // Show Loading View
                    }
                }
                .onSubmit {
                    runSearch()
                }
            }
            .padding(.horizontal, 8)
            .padding(.vertical, 4)
            .cornerRadius(5)
            if isEditingSearchBar {
                Button(action: {
                    self.isEditingSearchBar = false
                    self.searchText = ""
                    self.resultsAreEmpty = false
                    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                }) {
                    Text("Cancel")
                }
                .padding(.trailing, 10)
                .transition(.move(edge: .trailing))    
                .foregroundColor(.blue)
            }
        }
        .padding(.top, 8)
        .padding(.bottom, 8)
        .background(Color.init(uiColor: .darkGray))
    }

    func runSearch() {
        // Send search to the server
    }
}

extension View {
    func placeholder<Content: View>(
         when shouldShow: Bool,
         alignment: Alignment = .leading,
         @ViewBuilder placeholder: () -> Content) -> some View {
         ZStack(alignment: alignment) {
            placeholder().opacity(shouldShow ? 1 : 0)
            self
        }
    }
}

Join our newsletter community

Oh no, there was an error with your email!

Hey, thank you so much for signing up! We've got your address saved, so look forward to an email from us soon. 🎉

We respect your privacy.