How to make a TextView look like a system search bar
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
}
}
}