Pattern · Lists and Feeds

New

Search Panel SwiftUI Block

The body of a search screen: recent and suggested searches, live suggestions with the match highlighted, and results by scope.

Drop it under a `.searchable` field. Before typing it shows recent searches with remove and Clear all and suggested searches as chips; while typing it asks your closure for debounced suggestions with the typed text highlighted; after a search it shows scope chips with counts and a card of results per scope, with loading, failed and no-results states built in.

  • SwiftUI
  • iOS 17.0+
  • search-panel
Get SwiftPieces Pro

$199 once, every piece included. See how installing works · Sign in

01

What's included

SwiftUI source you own, built on Apple frameworks only.

  • Recent searches with remove and Clear all, kept for you
  • Suggested searches as chips before typing
  • Debounced live suggestions with the typed text in semibold
  • Results by scope with counts and Show all
  • Loading shapes, no results with a hint, and Try again on failure

02

Implementation notes

How it behaves, what it handles, and the decisions behind it.

Before anything is typed, the panel shows Recent searches on a card, each with a remove button and Clear all above them, and Suggested searches as outlined chips. As soon as the person types, it switches to Suggestions: your suggest closure runs 250 milliseconds after typing stops, a small spinner sits beside the title while it works, and each suggestion shows the typed text in semibold primary ink with the rest in the secondary ink, with a fill-in arrow to complete the field without searching and a last row that searches for exactly what was typed. When a search is submitted from the keyboard, a recent search, a chip or a suggestion, the panel runs your search closure behind grey shapes and then shows the results by scope: a row of chips with counts (All 9, Places 4, Guides 3, People 2), and under All a card per scope with its first two results and Show all, which selects that scope. Results have a symbol or initials, the title with the match in semibold, a line under it and a short trailing detail. A search with no results says so with a hint, and a failed one shows its message with Try again. Every submitted search moves to the top of Recent.

Notes

  • SearchPanel(query:submitted:recents:suggested:scopes:suggest:search:onSelect:) takes three bindings and three closures. It is a section: no navigation stack, background, padding or scroll view of its own. Put it in the ScrollView that carries .searchable(text: $query) and .onSubmit(of: .search) { submitted = query }; SwiftUI only delivers the keyboard's Search to a submit handler on the host.
  • query is the field's text. submitted is the last submitted search: results show while it matches the trimmed query, and editing the field goes back to suggestions. Tapping a recent search, a chip or a suggestion sets both.
  • recents is newest first. The block inserts submitted searches (without duplicates, up to eight), removes one with its button and clears them all; persist the array however your app stores preferences.
  • suggested are the chips shown before typing. Leave it empty to hide the section; with neither recents nor suggestions the panel shows a short prompt built from the scope names.
  • SearchPanel.Scope is an id and a title, in chip order; SearchPanel.sampleScopes is Places, Guides and People. Only scopes with results get a chip.
  • SearchPanel.Result holds an id, its scope id, the title, an optional subtitle, a short detail and an optional SF Symbol; without a symbol the row shows the title's initials, for people. Result.samples is the catalog from the preview.
  • suggest(text) is debounced and cancelled when the text changes, so a slow answer never replaces a newer one. A thrown error hides the suggestions quietly; the field still submits.
  • search(text) runs once per submitted search and the results are kept while the text stays the same. A thrown error shows its errorDescription (or "Check your connection and try again.") with Try again; CancellationError is ignored.
  • onSelect is called when a result is tapped.
  • Haptics mark a selection when the scope changes.
  • VoiceOver hears headers for each section, labels for the remove and fill-in buttons, each result as its title, subtitle and detail, and an announcement with the result count.
  • Reduce Motion turns every change into a short fade.
  • SearchPanelExample at the end of SearchPanel.swift is the Explore screen in the preview: a travel guide with three recent searches, four suggestions, a small catalog for suggestions and a search that returns places, guides and people. Pass wrapsInNavigationStack: false to push it in your own stack.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • Cards are color.surface with radius.lg, rows at least 52 points, SPHairline dividers in separator inset to the text, and sections 24 points apart (space.xl). Section titles are type.headline.
  • Rows use type.body, subtitles and details type.footnote in textSecondary and textTertiary, symbols textSecondary. Result symbols and initials sit in 40 point circles on fill.
  • Suggested chips are surface capsules with a separator stroke. The selected scope chip is textPrimary with bg text; the rest match the suggested chips. Counts use monospaced digits.
  • Highlighted text is semibold textPrimary against textSecondary in suggestions and against textPrimary in results.
  • Try again is the secondary button: fill at radius.md, 52 points, type.headline.
  • Changes animate with motion.smooth and motion.snappy, and become short fades under Reduce Motion.