如何用 SwiftUI 显示搜索栏

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

2019年编辑: 目前的解决方案是使用 TextField作为搜索栏,但它没有搜索图标。

enter image description here

47989 次浏览

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

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

这个 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)

这里是一个纯粹的 SwiftUI 版本,基于 Antoine Weber 的对他上述问题的回答,以及我在 这个博客这个要点中发现的内容。它包含了

  • 一个清晰的按钮,
  • 取消按钮,
  • 在拖动列表和
  • 在选择搜索文本字段时隐藏导航视图。

可以通过在 UIApplication 窗口中 这些答案之后的方法实现在拖动列表中重新设置键盘。为了便于处理,我在 UIApplication 和 View 修饰符上创建了一个扩展,最后是 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

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

这种方法的优点是实现了所有预期的行为,包括滚动条上的自动隐藏/显示、清除和取消按钮、键盘上的搜索键等。

包装 UINavigationController的搜索栏也可以确保任何新的变化,由苹果对他们自动采用在您的项目。

输出示例

点击这里查看实施情况

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

我也写了一个 文章关于这一点,它可能会得到进一步的澄清。

希望这个有用,干杯!

IOS 15.0 +

MacOS 12.0 + ,Mac Catalyst 15.0 + ,tvOS 15.0 + ,watch OS 8.0 +

searchable(_:text:placement:)

将此视图标记为可搜索,它配置搜索字段的显示

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 中的工艺搜索体验

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

我迟到了,但看起来你可以

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

当你向下滚动时,displayMode.always将确保它粘附在顶部

IOS14 +

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