Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - Search in List Header

I am trying to to recreate what everyone know from UITableView with SwiftUI: A simple search field in the header of the tableview:

A simple search field in the header of the tableview

However, the List View in SwiftUI does not even seem to have a way to add a header or footer. You can set a header with a TextField to sections like this:

@State private var searchQuery: String = ""

var body: some View {

List {
    Section(header:
        Group{
            TextField($searchQuery, placeholder: Text("Search"))
                                .background(Color.white)
            }) {
                  ListCell()
                  ListCell()
                  ListCell()
                }

    }
}

However, I am not sure if this is the best way to do it because:

  1. The header does not hide when you scroll down as you know it from UITableView.
  2. The SearchField does not look like the search field we know and love.

Has anyone found a good approach? I don't want to fall back on UITableView.

like image 523
Daniel Avatar asked Jun 15 '19 09:06

Daniel


4 Answers

Xcode 13 / SwiftUI 3

You can now use .searchable to make a List... searchable!

struct ContentView: View {

    @State private var searchQuery: String = ""

    var body: some View {
        NavigationView {
            List {
                ForEach(Array(1...100)
                            .map { "\($0)" }
                            .filter { searchQuery.isEmpty ? true : $0.contains(searchQuery) }
                        ,id: \.self) { item in
                    Text(verbatim: item)
                }
            }
            .navigationTitle("Fancy Numbers")
            .searchable(text: $searchQuery)
        }
    }

}

The search bar seems to appear only if the List is embedded in a NavigationView.


Xcode 12, SwiftUI 1/2

You can port UISearchBar to SwiftUI.

(More about this can be found in the excellent WWDC 2019 talk - Integrating SwiftUI)

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() -> Coordinator {
        return Coordinator(text: $text)
    }

    func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
        let searchBar = UISearchBar(frame: .zero)
        searchBar.delegate = context.coordinator
        return searchBar
    }

    func updateUIView(_ uiView: UISearchBar,
                      context: UIViewRepresentableContext<SearchBar>) {
        uiView.text = text
    }
}

And use it like this:

struct ContentView: View {

    @State private var searchQuery: String = ""

    var body: some View {

        List {
            Section(header: SearchBar(text: self.$searchQuery)) {
                ForEach(Array(1...100).filter {
                    self.searchQuery.isEmpty ?
                        true :
                        "\($0)".contains(self.searchQuery)
                }, id: \.self) { item in
                    Text("\(item)")
                }
            }
        }
    }
}

It's the proper search bar, but it doesn't hide - I'm sure we'll be able to do it at some point via SwiftUI API.

Looks like this:

enter image description here

like image 156
Matteo Pacini Avatar answered Oct 27 '22 16:10

Matteo Pacini


Perhaps a starting point here. Consider using ZStack for disappear effect on scroll for scrollview.


struct ContentView: View {
    @State var search: String

    var body: some View {
        NavigationView {
            VStack(alignment: .center, spacing: 0) {
                HStack {
                    Image(systemName: "magnifyingglass")
                        .padding(.leading, CGFloat(10.0))
                    TextField("Search", text: $search, onEditingChanged: { active in
                        print("Editing changed: \(active)")
                    }, onCommit: {
                        print("Commited: \(self.search)")
                    })
                        .padding(.vertical, CGFloat(4.0))
                        .padding(.trailing, CGFloat(10.0))
                }
                    .overlay(
                        RoundedRectangle(cornerRadius: 5.0)
                            .stroke(Color.secondary, lineWidth: 1.0)
                    )
                    .padding()
                List {
                    ForEach(0...100, id: \.self) { e in
                        Text("Item \(e)")
                    }
                }
            }
            .navigationBarTitle(title(for: self.search))
        }
    }

    private func title(for value: String?) -> String {
        guard let value = value, value.count > 0 else {
            return "No search"
        }

        return #"Searching for "\#(value)""#
    }
}

Screenshot

like image 5
Florent Morin Avatar answered Oct 27 '22 18:10

Florent Morin


I implemented my country picker project Columbus using SwiftUI. I implemented a custom publisher CountryListViewModel and connected that with the text field. This way I can type, search the data source, filter out results / debounce and update the table view when all operations are done. Works pretty well 👍

https://github.com/Blackjacx/Columbus/tree/swift-ui/Source/Classes

like image 2
blackjacx Avatar answered Oct 27 '22 16:10

blackjacx


2021 — Xcode 13 / SwiftUI 3

Native SwiftUI solution:

List {
    ForEach(0..<5) {
        index in
            Text("item \(index)")
        }
    }
}
.searchable(text: .constant("search_value")) // should be a Binding
like image 1
ixany Avatar answered Oct 27 '22 16:10

ixany