如何使用 SwiftUI 显示搜索栏
How to display a search bar with SwiftUI
新的SwiftUI框架好像没有提供内置的搜索栏组件。我应该使用 UISearchController 并以某种方式包装它,还是应该使用简单的文本字段并根据文本字段输入更新数据?
2019 编辑:当前的解决方法是使用 TextField
作为搜索栏,但它没有搜索图标。
许多 UIKit 组件目前没有 SwiftUI 等价物。为了使用它们,您可以创建一个包装器,如 documentation.
所示
基本上,您制作一个符合 UIViewRepresentable
并实现 makeUIView
和 updateUIView
.
的 SwiftUI class
这个 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)
这里是纯swiftUI版本,基于Antoine Weber's answer to his question above and what I found in this blog and this gist。它包含
- 一个清除按钮,
- 一个取消按钮,
- 在列表中拖动时退出键盘
- 选择搜索文本字段时隐藏导航视图。
可以使用 these answers 之后的 UIApplication window 上的方法来实现在列表中拖动时放弃键盘。为了更容易处理,我在 UIApplication 上创建了一个扩展,并为此扩展创建了视图修饰符,最后是对 View:
的扩展
// Deprecated with iOS 15
//extension UIApplication {
// func endEditing(_ force: Bool) {
// self.windows
// .filter{[=10=].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]( 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{[=12=].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{[=12=].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]( 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())
}
}
搜索栏的最终结果,最初显示时如下所示
当搜索栏被编辑成这样时:
在行动:
通过包装 UINavigationController
.
可以在 SwiftUI
中正确实现原生搜索栏
这种方法为我们提供了实现所有预期行为的优势,包括自动 hide/show 滚动、清除和取消按钮以及键盘中的搜索键等。
为搜索栏包装 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()
}
}
}
以上代码可以使用as-is(也可以of-course修改以适应项目的具体需要)。
该视图包括 'search' 和 '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,可以参考一下以获得额外的说明。
希望对您有所帮助,干杯!
iOS 15.0+
macOS 12.0+,Mac Catalyst 15.0+,tvOS 15.0+,watchOS 8.0+
searchable(_:text:placement:)
Marks this view as searchable, which configures the display of a search field. 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 中的 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 { [=10=].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
将确保它在您向下滚动时保持在 t0p
新的SwiftUI框架好像没有提供内置的搜索栏组件。我应该使用 UISearchController 并以某种方式包装它,还是应该使用简单的文本字段并根据文本字段输入更新数据?
2019 编辑:当前的解决方法是使用 TextField
作为搜索栏,但它没有搜索图标。
许多 UIKit 组件目前没有 SwiftUI 等价物。为了使用它们,您可以创建一个包装器,如 documentation.
所示基本上,您制作一个符合 UIViewRepresentable
并实现 makeUIView
和 updateUIView
.
这个 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)
这里是纯swiftUI版本,基于Antoine Weber's answer to his question above and what I found in this blog and this gist。它包含
- 一个清除按钮,
- 一个取消按钮,
- 在列表中拖动时退出键盘
- 选择搜索文本字段时隐藏导航视图。
可以使用 these answers 之后的 UIApplication window 上的方法来实现在列表中拖动时放弃键盘。为了更容易处理,我在 UIApplication 上创建了一个扩展,并为此扩展创建了视图修饰符,最后是对 View:
的扩展
// Deprecated with iOS 15
//extension UIApplication {
// func endEditing(_ force: Bool) {
// self.windows
// .filter{[=10=].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]( 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{[=12=].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{[=12=].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]( 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())
}
}
搜索栏的最终结果,最初显示时如下所示
当搜索栏被编辑成这样时:
在行动:
通过包装 UINavigationController
.
SwiftUI
中正确实现原生搜索栏
这种方法为我们提供了实现所有预期行为的优势,包括自动 hide/show 滚动、清除和取消按钮以及键盘中的搜索键等。
为搜索栏包装 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()
}
}
}
以上代码可以使用as-is(也可以of-course修改以适应项目的具体需要)。
该视图包括 'search' 和 '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,可以参考一下以获得额外的说明。
希望对您有所帮助,干杯!
iOS 15.0+
macOS 12.0+,Mac Catalyst 15.0+,tvOS 15.0+,watchOS 8.0+
searchable(_:text:placement:)
Marks this view as searchable, which configures the display of a search field. 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 中的 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 { [=10=].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
将确保它在您向下滚动时保持在 t0p