The new SwiftUI framework does not seem to provide a built-in search bar component. Should I use a UISearchController and wrap it in some way, or should I use a simple textfield and update the data according to the textfield input?
EDIT: the current workaround is to use a TextField
as a searchBar. It is working very well, but it doesn't have the search icon
import SwiftUI struct Search : View { let array = ["John","Lena","Steve","Chris","Catalina"] @State private var searchText = "" var body: some View { NavigationView{ List{ TextField("Type your search",text: $searchText) .textFieldStyle(RoundedBorderTextFieldStyle()) ForEach(array.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self){searchText in Text(searchText) } } .navigationBarTitle(Text("Search")) } } } struct Search_Previews : PreviewProvider { static var previews: some View { Search() } }
updated to work with Xcode 11.1
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.
Here is a pure swiftUI version, based on Antoine Weber's answer to his question above and what I found in this blog and this gist. It incorporates
Resigning the keyboard on drag in the list can be realized using a method on UIApplication window following these answers. For easier handling I created an extension on UIApplication and view modifier for this extension and finally an extension to View:
extension UIApplication { func endEditing(_ force: Bool) { self.windows .filter{$0.isKeyWindow} .first? .endEditing(force) } } struct ResignKeyboardOnDragGesture: ViewModifier { var gesture = DragGesture().onChanged{_ in UIApplication.shared.endEditing(true) } func body(content: Content) -> some View { content.gesture(gesture) } } extension View { func resignKeyboardOnDragGesture() -> some View { return modifier(ResignKeyboardOnDragGesture()) } }
So the final modifier for resigning the keyboard is just one modifier that has to be placed on the list like this:
List { ForEach(...) { //... } } .resignKeyboardOnDragGesture()
The complete swiftUI project code for the search bar with a sample list of names is as follows. You can paste it into ContentView.swift of a new swiftUI project and play with it.
import SwiftUI struct ContentView: View { let array = ["Peter", "Paul", "Mary", "Anna-Lena", "George", "John", "Greg", "Thomas", "Robert", "Bernie", "Mike", "Benno", "Hugo", "Miles", "Michael", "Mikel", "Tim", "Tom", "Lottie", "Lorrie", "Barbara"] @State private var searchText = "" @State private var showCancelButton: Bool = false var body: some View { NavigationView { VStack { // Search view HStack { HStack { Image(systemName: "magnifyingglass") TextField("search", text: $searchText, onEditingChanged: { isEditing in self.showCancelButton = true }, onCommit: { print("onCommit") }).foregroundColor(.primary) Button(action: { self.searchText = "" }) { Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1) } } .padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6)) .foregroundColor(.secondary) .background(Color(.secondarySystemBackground)) .cornerRadius(10.0) if showCancelButton { Button("Cancel") { UIApplication.shared.endEditing(true) // this must be placed before the other commands here self.searchText = "" self.showCancelButton = false } .foregroundColor(Color(.systemBlue)) } } .padding(.horizontal) .navigationBarHidden(showCancelButton) // .animation(.default) // animation does not work properly List { // Filtered list of names ForEach(array.filter{$0.hasPrefix(searchText) || searchText == ""}, id:\.self) { searchText in Text(searchText) } } .navigationBarTitle(Text("Search")) .resignKeyboardOnDragGesture() } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { Group { ContentView() .environment(\.colorScheme, .light) ContentView() .environment(\.colorScheme, .dark) } } } extension UIApplication { func endEditing(_ force: Bool) { self.windows .filter{$0.isKeyWindow} .first? .endEditing(force) } } struct ResignKeyboardOnDragGesture: ViewModifier { var gesture = DragGesture().onChanged{_ in UIApplication.shared.endEditing(true) } func body(content: Content) -> some View { content.gesture(gesture) } } extension View { func resignKeyboardOnDragGesture() -> some View { return modifier(ResignKeyboardOnDragGesture()) } }
The final result for the search bar, when initially displayed looks like this
and when the search bar is edited like this:
In Action:
A native Search Bar can be properly implemented in SwiftUI
by wrapping the UINavigationController
.
This approach gives us the advantage of achieving all the expected behaviours including automatic hide/show on scroll, clear and cancel button, and search key in the keyboard among others.
Wrapping the UINavigationController
for Search Bar also ensures that any new changes made to them by Apple are automatically adopted in your project.
Click here to see the implementation in action
import SwiftUI struct SearchNavigation<Content: View>: UIViewControllerRepresentable { @Binding var text: String var search: () -> Void var cancel: () -> Void var content: () -> Content func makeUIViewController(context: Context) -> UINavigationController { let navigationController = UINavigationController(rootViewController: context.coordinator.rootViewController) navigationController.navigationBar.prefersLargeTitles = true context.coordinator.searchController.searchBar.delegate = context.coordinator return navigationController } func updateUIViewController(_ uiViewController: UINavigationController, context: Context) { context.coordinator.update(content: content()) } func makeCoordinator() -> Coordinator { Coordinator(content: content(), searchText: $text, searchAction: search, cancelAction: cancel) } class Coordinator: NSObject, UISearchBarDelegate { @Binding var text: String let rootViewController: UIHostingController<Content> let searchController = UISearchController(searchResultsController: nil) var search: () -> Void var cancel: () -> Void init(content: Content, searchText: Binding<String>, searchAction: @escaping () -> Void, cancelAction: @escaping () -> Void) { rootViewController = UIHostingController(rootView: content) searchController.searchBar.autocapitalizationType = .none searchController.obscuresBackgroundDuringPresentation = false rootViewController.navigationItem.searchController = searchController _text = searchText search = searchAction cancel = cancelAction } func update(content: Content) { rootViewController.rootView = content rootViewController.view.setNeedsDisplay() } func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { text = searchText } func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { search() } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { cancel() } } }
The above code can be used as-is (and can of-course be modified to suit the specific needs of the project).
The view includes actions for 'search' and 'cancel' which are respectively called when the search key is tapped on the keyboard and the cancel button of the search bar is pressed. The view also includes a SwiftUI
view as a trailing closure and hence can directly replace the NavigationView
.
import SwiftUI struct YourView: View { // Search string to use in the search bar @State var searchString = "" // Search action. Called when search key pressed on keyboard func search() { } // Cancel action. Called when cancel button of search bar pressed func cancel() { } // View body var body: some View { // Search Navigation. Can be used like a normal SwiftUI NavigationView. SearchNavigation(text: $searchString, search: search, cancel: cancel) { // Example SwiftUI View List(dataArray) { data in Text(data.text) } .navigationBarTitle("Usage Example") } .edgesIgnoringSafeArea(.top) } }
I have also written an article on this, it may be referred to get additional clarification.
I hope this helps, cheers!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With