16

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.

Daniel
  • 1,473
  • 3
  • 33
  • 63

4 Answers4

34

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

Matteo Pacini
  • 21,796
  • 7
  • 67
  • 74
  • Thanks, using UISearchBar is probably the best idea here. – Daniel Jun 15 '19 at 15:50
  • @Daniel until SwiftUI gets better :) – Matteo Pacini Jun 15 '19 at 16:05
  • Yes, I'm sure Apple has a lot planned with SwiftUI in the future. – Daniel Jun 15 '19 at 17:40
  • This crashes the compiler for me (tried it in a Playground on Mojave, maybe that has something to do with it?). – cargath Jun 20 '19 at 06:55
  • @cargath It worked for me on Mojave. What error did you get? – iComputerfreak Jun 26 '19 at 19:17
  • I'm getting an error in the coordinator init "Cannot assign to property: '$text' is immutable" I'm using beta 4 – Rob Jul 19 '19 at 18:37
  • @Rob, same here in beta 4, does anyone knows where can I find some documentation on any changes made to these things? – gujci Jul 24 '19 at 12:16
  • Make the Coordinator: `var text: Binding`. Then in the `init` do `self.text = text`. Then to change the text do `text.value = searchText`. – keji Jul 28 '19 at 21:38
  • 7
    You can get a better look if you do not put it in the header. Instead, place it in a VStack with 0 spacing and containing the search bar and the list: `VStack(alignment: .center, spacing: 0) { SearchBar(text: self.$searchQuery); List { ...`. This way you get it to use the full width, and the search bar remains at the top at all times. – kvaruni Aug 13 '19 at 15:09
  • @kvaruni: Also the performance of the search is much better when you do not put the searchBar in the header. I have a tableView with >1000 items and the section-solution performes poor. – Klaus Dec 08 '19 at 20:12
5

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

Florent Morin
  • 842
  • 1
  • 10
  • 13
2

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

blackjacx
  • 9,011
  • 7
  • 45
  • 56
1

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
ixany
  • 5,433
  • 9
  • 41
  • 65