【问题标题】:How to display a search bar with SwiftUI如何使用 SwiftUI 显示搜索栏
【发布时间】:2019-10-22 18:18:39
【问题描述】:

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

编辑:当前的解决方法是使用TextField 作为搜索栏。它运行良好,但没有搜索图标

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

更新为与 Xcode 11.1 一起使用

【问题讨论】:

  • 我发现这个教程真的很有帮助:mecid.github.io/2019/06/05/swiftui-making-real-world-app
  • 所以他使用的是文本字段而不是搜索栏
  • 对于目前尝试复制此功能的人来说,TextFieldStyle 现在是一个协议,所以.textFieldStyle(.roundedBorder) 现在是.textFieldStyle(RoundedBorderTextFieldStyle())

标签: swiftui


【解决方案1】:

这是一个纯 swiftUI 版本,基于Antoine Weber's 对他上面问题的回答以及我在this blogthis gist 中找到的内容。它包含

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

在列表中拖动时退出键盘可以使用these answers之后的UIApplication窗口上的方法实现。为了更容易处理,我在 UIApplication 上创建了一个扩展,并为此扩展创建了视图修饰符,最后是对 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())
    }
}

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

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

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

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

当搜索栏被这样编辑时:

在行动:

【讨论】:

  • 感谢您的精彩回答!它工作得非常好,除了一个小问题。将列表放入 VStack 将导致导航栏保持其高度,即使列表已向下滚动。请问有什么解决方法吗?
  • 不确定,你指的是什么。这是预期的行为。例如,如果您查看设置中的搜索:搜索栏一直位于最顶部,直到搜索被取消,就像上面的视频中一样。但也许我误解了你的问题。如果您删除 .navigationBarHidden(showCancelButton),导航栏将保持原样,其下方的搜索栏将保持不变。但这会减少列表的空间。
  • 我的意思是在非搜索模式下滚动列表时,导航栏不会折叠。这是因为 List 放在 VStack 中,而不是直接放在 NavigationView 中。只是想知道有没有其他方法可以恢复导航栏自动折叠行为。
  • 我还没有找到折叠导航栏的好方法。一种解决方法可能是删除 VStack 并将搜索栏作为列表的第一个元素(在 ForEach 之前),然后滚动时它将在导航栏下方移动。这为列表提供了更多空间,但缺点是搜索栏并不总是可见。
  • 出色的答案,除了小问题:(1)滚动时折叠导航栏。 (2) 显示/隐藏导航栏的动画不适合代码里面的注释写的
【解决方案2】:

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

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)

【讨论】:

  • 这仍然适用于最新的测试版吗?对我来说,来自协调员的文本会更新,但不会更进一步(SearchBar 文本和查看 searchTerm)
  • 它对我有用。你能分享一些显示你的问题的代码片段吗?
【解决方案3】:

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

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

为搜索栏包装 UINavigationController 还可以确保 Apple 对其所做的任何新更改都会自动在您的项目中采用。

示例输出

Click here to see the implementation in action

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

我也为此写了article,可以参考它以获得额外的澄清。

我希望这会有所帮助,干杯!

【讨论】:

  • 嘿,Yugantar,刚刚尝试过这个,它比我以前见过的所有东西都好用……只要你不使用 splitView。为了让它工作,我必须将你的 SearchNavigation 作为第一个元素嵌入到 NavigationView 中,它确实编译但看起来非常糟糕。一个小的改进是在外部 NavigationView 上添加 .navigatoinBarHidden(true),但不幸的是,光学仍然不可接受。我想需要包装整个 UISplitViewController 来解决这个问题。
  • @ShadowLightz 哦,太好了! SplitView 到底有什么问题?我们的项目专注于 iOS,所以我不知道...
  • @YugantarJain 刚刚尝试过。似乎 splitView 不能用作拆分视图,而是充当常规导航视图,即第二个屏幕推到前一个屏幕上。状态栏周围也有一些奇怪的样式,几乎就像背景是一种截然不同的颜色。
  • @ChrisEdwards 没错,你只是在 iPad 上得到了一个放大的 iPhone 应用程序。或者,当在 SearchNavigation 中嵌入额外的 NavigationView 时,您会得到一个拆分视图,但还有一个跨越两列的搜索栏。反过来嵌入会产生一个额外的导航标题区域。我尝试包装 UISplitViewController,但我对这个结果也很不满意。不过可以分享代码。
  • @ChrisEdwards 我不确定拆分视图,但是可以通过在搜索导航视图上使用 .edgesIgnoringSafeArea(.top) 来修复具有不同颜色的状态栏。我已经用相同的方式更新了代码(正在使用中)。
【解决方案4】:

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 视频了解更多信息

Craft search experiences in SwiftUI

【讨论】:

    【解决方案5】:

    这适用于 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)
        }
    }
    

    【讨论】:

      【解决方案6】:

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

      基本上,您创建一个符合UIViewRepresentable 并实现makeUIViewupdateUIView 的SwiftUI 类。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-01-30
        • 1970-01-01
        • 2016-09-04
        • 2021-01-25
        • 1970-01-01
        • 2017-03-31
        • 1970-01-01
        相关资源
        最近更新 更多