Live-Filtering Search Bar in Swift
Problem Implement a search bar in Swift such that, as the user types, the list below narrows live to only the items whose names match the entered text.
Requirements
@State private var query: Stringbound to the text fieldfilteredItems: [Item]— derived fromitemsandquery, recomputed as the query changes- Prefix match (
hasPrefix) or substring match (contains), case-insensitive - Empty query shows the full list; no match shows an explicit empty state
Core design
- Bind the text field to an observable state variable (
TextField("Search", text: $query)in SwiftUI, orUISearchResultsUpdatingin UIKit). Every keystroke mutates state and triggers a re-render. - Filtering is a derived computed property, not a second stored array:
items.filter { $0.name.lowercased().hasPrefix(query.lowercased()) }. Storing a separatefilteredItemsalongsideitemscreates two sources of truth that drift the moment the backing data updates. - Normalize once per keystroke, not per element — hoist
query.lowercased()out of the closure so an n-element filter does one lowercase instead of n. - Prefix vs. contains is a product decision worth surfacing:
hasPrefixmatches how users think about names and is indexable;containsfinds more but returns noisier results. - Empty query must short-circuit to the full list rather than filtering against
"", and the no-match case needs its own view rather than an ambiguous blank list.
Discussion points
- Debouncing: filtering on every keystroke is fine for a few hundred in-memory items but wasteful for large sets and untenable when each keystroke is a network call. Discuss a 250-300ms debounce, and that a debounced async search must discard out-of-order responses or a slow early request can overwrite a newer result.
- Case and locale:
lowercased()handles ASCII but not diacritics —folding(options: [.diacriticInsensitive, .caseInsensitive])orlocalizedCaseInsensitiveContainsis the correct tool for real user data. - Trade-off: linear
filteris O(n) per keystroke and trivially correct; a prefix tree (trie) or a pre-sorted array with binary search gives sub-linear lookup, justified only once n is large. - Cancellation: an in-flight search must be cancelled when the query changes, or results flicker between stale and fresh.
- Rendering: on large result sets the filter is rarely the bottleneck — list diffing is. Stable identity on rows keeps SwiftUI from rebuilding the whole list.
- Extension: fuzzy matching and highlighting matched substrings in the row, which requires the filter to return match ranges rather than a bare boolean.
asked …