Reputation: 1904
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 textfield and update the data according to the textfield input?
2019 EDIT: A current workaround is to use a TextField
as a searchBar but it does not have the search icon.
Upvotes: 79
Views: 58130
Reputation: 21
While other answers may work, SwiftUI now provides a component.
You can use :
@State private var text = ""
var body: some View {
NavigationStack {
VStack {
//Your content
}.searchable(text: $text, prompt: "Search" )
.navigationTitle("Search")
}
}
After that you can just create a variable to filter the text, and have the ui shown be the filtered text.
Upvotes: 0
Reputation: 5767
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(""))
}
}
Upvotes: 1
Reputation: 2236
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
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:
// 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())
}
}
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)
}
}
}
// 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())
}
}
The final result for the search bar, when initially displayed looks like this
and when the search bar is edited like this:
In Action:
Upvotes: 142
Reputation: 315
I am late to this. But it looks like you can just use
searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search")
The displayMode .always
will make sure it stick to the t0p when you scroll down
Upvotes: 5
Reputation: 16114
Many UIKit components currently do not have SwiftUI equivalents. In order to use them, you can create a wrapper as shown in documentation.
Basically, you make a SwiftUI class that conforms to UIViewRepresentable
and implements makeUIView
and updateUIView
.
Upvotes: 2
Reputation: 221
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.
Click here to see the implementation in action
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
.
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!
Upvotes: 22
Reputation: 14935
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)
}
}
Craft search experiences in SwiftUI
Upvotes: 21
Reputation: 2293
This is for iOS 15.0+ in SwiftUI.
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)
}
}
Upvotes: 2
Reputation: 716
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)
Upvotes: 23