Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to display a search bar with SwiftUI

Tags:

swiftui

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

enter image description here

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

like image 655
Antoine Weber Avatar asked Jun 07 '19 08:06

Antoine Weber


People also ask

How do I add a search bar in SwiftUI?

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.


2 Answers

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

  • a clear button,
  • a cancel button,
  • resigning keyboard on dragging in the list and
  • hiding the navigation view when the search text field is selected.

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

enter image description here

and when the search bar is edited like this:

enter image description here

In Action:

enter image description here

like image 104
user3687284 Avatar answered Oct 07 '22 23:10

user3687284


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.

Example Output

Click here to see the implementation in action

Code (wrap UINavigationController):

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.

Usage (in SwiftUI View):

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!

like image 27
Yugantar Jain Avatar answered Oct 08 '22 00:10

Yugantar Jain