Reputation: 4994
I have created a custom search bar with TextField
and trying to filter my List with it.
[![enter image description here][1]][1]
My list consists of A to Z sections. Now I have a problem with filtering both section and list while searching which is returning nil. Here is my code:
@State private var searchText = ""
let lists = loadGlossary().keys.sorted()
let glossary = loadGlossary()
var body: some View {
VStack(spacing: -10) {
//Navigation bar
NavigationBarView(title: "Glossary", action: {}, titleColor: .primary, closeColor: .primary.opacity(0.5))
.padding()
//Search Bar
TextField("Search", text: $searchText)
.padding()
.frame(height: 45)
.background(.gray.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding()
.overlay {
HStack {
Spacer()
Button {
searchText = ""
} label: {
Label("clear", systemImage: "xmark.circle.fill")
.foregroundColor(.gray)
.opacity(searchText.isEmpty ? 0 : 1)
.padding(30)
}
.labelStyle(.iconOnly)
}
}
//List
List {
ForEach(Array(wordDict.keys).sorted(by: <), id: \.self) { character in
///Section
Section(header: Text("\(character)").font(Font.system(size:18, weight: .bold, design: .serif)).foregroundColor(.primary)) {
///List
ForEach(wordDict[character] ?? [""], id: \.self) { word in
VStack(alignment: .leading, spacing: 5) {
Text(word)
.font(.system(size: 17, weight: .bold, design: .serif))
Divider()
Text(getDescription(word))
.foregroundColor(.secondary)
}
}
.padding()
}
}
.listRowSeparator(.hidden)
}
.listStyle(.plain)
.padding(.top)
}
}
I would be grateful if you help to properly filter sections and items.
EDITED: Changes:
@State private var wordDict: [String:[String]] = [:]
func loadData() -> [String:[String]] {
let letters = Set(lists.compactMap( { $0.first } ))
var dict: [String:[String]] = [:]
for letter in letters {
dict[String(letter)] = lists.filter( { $0.first == letter } ).sorted()
}
return dict
}
and load the first data here:
.onAppear {
wordDict = loadData()
}
and for TextField
I have added:
TextField("Search", text: $searchText)
.onChange(of: searchText) {
wordDict = getFilteredWords(query: $0)
}
But when I clear the textfield of backspace the List won't update anymore
Upvotes: 1
Views: 3659
Reputation: 257729
Here is a possible way
@State private var filteredWords: [String: String] = [:]
ForEach(Array(filteredWords.keys).sorted(by: <), id: \.self) { character in
///Section
Section(header: Text("\(character)").font(Font.system(size:18, weight: .bold, design: .serif)).foregroundColor(.primary)) {
///List
ForEach(filteredWords[character] ?? [""], id: \.self) { word in
func load() {
// ...
self.wordDict = loadedData
// assuming possible re-load apply existed filter
self.filteredWords = applyFilter(data: loadedData, pattern: self.searchText)
}
TextField("Search", text: $searchText)
.onChange(of: searchText) {
self.filteredWords = applyFilter(data: self.wordDict, pattern: $0)
}
or with view model and publishers to debounce (example)
Upvotes: 0
Reputation: 971
One approach would be that you could do the filtering like this:
Here is the function for your specific need under the following assumptions:
[String: [String]]
. (e.g. "A": ["apple", "Atari"])searchText
. If you want to have more advanced querying capabilities the method would be the same, you just have to make minor adjustments to the function below.func getFilteredWords(query: String) -> [String: [String]] {
wordDict.mapValues({ entry in
entry.filter({ word in word.lowercased().contains(query.lowercased()) })
}).filter({ entry in !entry.value.isEmpty})
}
What does the function do?
The function takes the searchText
and compares all of the entries of your wordDict
with it. If the wordDict
entry includes the part of searchText
somewhere, it is kept. Otherwise it is discarded and therefore later not rendered. The comparison is case insensitive. After that, all of the categories without any entries are also discarded which prevents that your search still shows all categories even if they don't contain any entries.
How to use it?
wordDict
instances in your List
View with getFilteredWords(query: searchText)
.Upvotes: 1