import SwiftUI struct SearchSuggestionsView: View { private struct CachedTag { let original: Either let names: [String] } var items: [Either] @Binding var searchText: String @Binding var suppressNextSearchSubmit: Bool @State private var cachedTags: [CachedTag] = [] private var lastSearchTag: String { String(searchText.split(separator: " ").last ?? "").lowercased() } private var itemsCacheKey: Int { items.reduce(into: Hasher()) { hasher, item in hasher.combine(item) } .finalize() } private func refreshCachedTags() { cachedTags = items.map { item in switch item { case .left(let tag): return CachedTag(original: item, names: [tag.name.lowercased()]) case .right(let query): return CachedTag(original: item, names: query.tags.map { $0.lowercased() }) } } } private var filteredItems: [Either] { let matchCandidateTag = lastSearchTag let matchingTagsLimit = 50 guard !matchCandidateTag.isEmpty else { return [] } var seenTags = Set() var matchingTags: [Either] = [] matchingTags.reserveCapacity(matchingTagsLimit) for tag in cachedTags { if matchingTags.count >= matchingTagsLimit { break } for name in tag.names { if name.contains(matchCandidateTag), seenTags.insert(name).inserted { matchingTags.append(tag.original) break } } } return matchingTags } var body: some View { Group { ForEach(filteredItems, id: \.self) { item in switch item { case .left(let tag): Button { let previousTags = searchText.split(separator: " ").dropLast() suppressNextSearchSubmit = true searchText = (previousTags.map(String.init) + [tag.name]).joined(separator: " ") } label: { Text(tag.name) } case .right(let query): let matchingTag = query.tags.first { tag in tag.lowercased().contains(lastSearchTag) } Button { if let matchingTag { let previousTags = searchText.split(separator: " ").dropLast() suppressNextSearchSubmit = true searchText = (previousTags.map(String.init) + [matchingTag]).joined(separator: " ") } } label: { if let matchingTag { Text(matchingTag) } else { Text(query.tags.first ?? "") } } } } } .onAppear { refreshCachedTags() } .onChange(of: itemsCacheKey) { refreshCachedTags() } } }