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: String bound to the text field
  • filteredItems: [Item] — derived from items and query, 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, or UISearchResultsUpdating in 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 separate filteredItems alongside items creates 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: hasPrefix matches how users think about names and is indexable; contains finds 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]) or localizedCaseInsensitiveContains is the correct tool for real user data.
  • Trade-off: linear filter is 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 …
LeaderboardSalaryAccount