how to display a search bar with SwiftUI

后端 未结 4 1725
深忆病人
深忆病人 2020-12-04 07:02

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 textf

相关标签:
4条回答
  • 2020-12-04 07:18

    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

    and when the search bar is edited like this:

    In Action:

    0 讨论(0)
  • 2020-12-04 07:19

    Many UIKit components currently do not have SwiftUI equivalents. In order to use them, you can create a wrapper as in https://developer.apple.com/tutorials/swiftui/creating-and-combining-views.

    Basically, you make a SwiftUI class that conforms to UIViewRepresentable and implements makeUIView and updateUIView.

    0 讨论(0)
  • 2020-12-04 07:29

    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!

    0 讨论(0)
  • 2020-12-04 07:33

    This YouTube video shows how it can be done. It boils down to:

    struct SearchBar: UIViewRepresentable {
    
        @Binding var text: String
    
        class Coordinator: NSObject, UISearchBarDelegate {
    
            @Binding var text: String
    
            init(text: Binding<String>) {
                _text = text
            }
    
            func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
                text = searchText
            }
        }
        func makeCoordinator() -> SearchBar.Coordinator {
            return Coordinator(text: $text)
        }
    
        func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
            let searchBar = UISearchBar(frame: .zero)
            searchBar.delegate = context.coordinator
            searchBar.autocapitalizationType = .none
            return searchBar
        }
    
        func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
            uiView.text = text
        }
    }
    

    and then instead of

    TextField($searchText)
                  .textFieldStyle(.roundedBorder)
    

    you use

    SearchBar(text: $searchText)
    
    0 讨论(0)
提交回复
热议问题