如何使用 SwiftUI 显示搜索栏

问题描述 投票:0回答:9

新的SwiftUI框架似乎没有提供内置的搜索栏组件。我应该使用 UISearchController 并以某种方式包装它,还是应该使用简单的文本字段并根据文本字段输入更新数据?

2019 编辑:当前的解决方法是使用

TextField
作为搜索栏,但它没有搜索图标。

enter image description here

swiftui
9个回答
141
投票

这是一个纯粹的 swiftUI 版本,基于 Antoine Weber 对他上面问题的回答以及我在 这个博客这个要点 中找到的内容。它包含

  • 一个清除按钮,
  • 取消按钮,
  • 在列表中拖动时放弃键盘并且
  • 选择搜索文本字段时隐藏导航视图。

在列表中拖动时放弃键盘可以使用 UIApplication 窗口上的方法来实现,遵循这些答案。为了更容易处理,我在 UIApplication 上创建了一个扩展,并为此扩展创建了视图修饰符,最后创建了 View 扩展:


// Deprecated with iOS 15
//extension UIApplication {
//    func endEditing(_ force: Bool) {
//        self.windows
//            .filter{$0.isKeyWindow}
//            .first?
//            .endEditing(force)
//    }
//}

// Update for iOS 15
// MARK: - UIApplication extension for resgning keyboard on pressing the cancel buttion of the search bar
extension UIApplication {
    /// Resigns the keyboard.
    ///
    /// Used for resigning the keyboard when pressing the cancel button in a searchbar based on [this](https://stackoverflow.com/a/58473985/3687284) solution.
    /// - Parameter force: set true to resign the keyboard.
    func endEditing(_ force: Bool) {
        let scenes = UIApplication.shared.connectedScenes
        let windowScene = scenes.first as? UIWindowScene
        let window = windowScene?.windows.first
        window?.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())
    }
}

因此,放弃键盘的最终修饰符只是一个必须放在列表中的修饰符,如下所示:

List {
    ForEach(...) {
        //...
    }
}
.resignKeyboardOnDragGesture()

搜索栏的完整 swiftUI 项目代码以及示例名称列表如下。您可以将其粘贴到新 swiftUI 项目的 ContentView.swift 中并使用它。


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)
        }
    }
}

// Deprecated with iOS 15
//extension UIApplication {
//    func endEditing(_ force: Bool) {
//        self.windows
//            .filter{$0.isKeyWindow}
//            .first?
//            .endEditing(force)
//    }
//}

// Update for iOS 15
// MARK: - UIApplication extension for resgning keyboard on pressing the cancel buttion of the search bar
extension UIApplication {
    /// Resigns the keyboard.
    ///
    /// Used for resigning the keyboard when pressing the cancel button in a searchbar based on [this](https://stackoverflow.com/a/58473985/3687284) solution.
    /// - Parameter force: set true to resign the keyboard.
    func endEditing(_ force: Bool) {
        let scenes = UIApplication.shared.connectedScenes
        let windowScene = scenes.first as? UIWindowScene
        let window = windowScene?.windows.first
        window?.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())
    }
}

搜索栏的最终结果最初显示时如下所示

enter image description here

当搜索栏被这样编辑时:

enter image description here

行动中:

enter image description here


23
投票

这个 YouTube 视频展示了如何做到这一点。归结为:

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
    }
}

然后代替

TextField($searchText)
              .textFieldStyle(.roundedBorder)

你使用

SearchBar(text: $searchText)

22
投票

通过包装

SwiftUI
,可以在
UINavigationController
中正确实现原生搜索栏。

这种方法使我们能够实现所有预期的行为,包括滚动时自动隐藏/显示、清除和取消按钮以及键盘中的搜索键等。

包裹搜索栏的

UINavigationController
还可以确保 Apple 对它们所做的任何新更改都会在您的项目中自动采用。

输出示例

单击此处查看实际实施情况

代码(包装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()
        }
    }
    
}

上面的代码可以按原样使用(当然也可以修改以适应项目的特定需求)。

该视图包括“搜索”和“取消”操作,当点击键盘上的搜索键并按下搜索栏的取消按钮时,将分别调用这些操作。该视图还包含一个

SwiftUI
视图作为尾随闭包,因此可以直接替换
NavigationView

用法(在 SwiftUI 视图中):

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)
    }
}

我还就此写了一篇文章,可以参考以获得更多说明。

我希望这有帮助,干杯!


20
投票

iOS 15.0+

macOS 12.0+、Mac Catalyst 15.0+、tvOS 15.0+、watchOS 8.0+

searchable(_:text:placement:)

将此视图标记为可搜索,这配置搜索字段的显示。 https://developer.apple.com/

struct DestinationPageView: View {
    @State private var text = ""
    var body: some View {
      NavigationView {
        PrimaryView()
        SecondaryView()
        Text("Select a primary and secondary item")
     }
     .searchable(text: $text)
  }
}

观看 WWDC 视频了解更多信息

在 SwiftUI 中打造搜索体验


5
投票

我迟到了。但看起来你可以直接使用

searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search")

显示模式

.always
将确保向下滚动时它坚持到t0p


2
投票

这适用于 SwiftUI 中的 iOS 15.0+

struct SearchableList: View {
    
    let groceries = ["Apple", "Banana", "Grapes"]
    @State private var searchText: String = ""
    
    var body: some View {
        NavigationView {
            List(searchResult, id: \.self) { grocerie in
                Button("\(grocerie)") { print("Tapped") }
            }
            .searchable(text: $searchText)
        }
    }
    
    var searchResult: [String] {
        guard !searchText.isEmpty else { return groceries }
        return groceries.filter { $0.contains(searchText) }
    }
}

struct SearchableList_Previews: PreviewProvider {
    static var previews: some View {
        SearchableList().previewLayout(.sizeThatFits)
    }
}

2
投票

许多 UIKit 组件目前没有 SwiftUI 等效项。为了使用它们,您可以创建一个包装器,如文档中所示。

基本上,您创建一个符合

UIViewRepresentable
并实现
makeUIView
updateUIView
的 SwiftUI 类。


1
投票

iOS 14+

import SwiftUI
import UIKit

struct SearchView: UIViewRepresentable {
    @Binding var text: String

    func makeUIView(context: Context) -> UISearchBar {
        let searchBar = UISearchBar()
        searchBar.backgroundImage = UIImage()
        searchBar.placeholder = "Search"
        searchBar.delegate = context.coordinator
        return searchBar
    }

    func updateUIView(_ uiView: UISearchBar, context: Context) {
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(text: $text)
    }

    class Coordinator: NSObject, UISearchBarDelegate {
        @Binding var text: String

        init(text: Binding<String>) {
            self._text = text
        }

        func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
            text = searchText
        }

        func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
            searchBar.setShowsCancelButton(true, animated: true)
        }

        func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
            searchBar.setShowsCancelButton(false, animated: true)
            searchBar.resignFirstResponder()
        }

        func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
            searchBar.endEditing(true)
            searchBar.text = ""
        }

        func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
            searchBar.endEditing(true)
        }
    }
}

struct SearchView_Previews: PreviewProvider {
    static var previews: some View {
        SearchView(text: .constant(""))
    }
}

0
投票

虽然其他答案可能有效,但 SwiftUI 现在提供了一个组件。

您可以使用:

@State private var text = ""
var body: some View {
    NavigationStack {
        VStack {
            //Your content
        }.searchable(text: $text, prompt: "Search" )
            .navigationTitle("Search")
       
    }
}

之后,您可以创建一个变量来过滤文本,并让显示的用户界面成为过滤后的文本。

© www.soinside.com 2019 - 2024. All rights reserved.