Pattern · Navigation Bars

New

Scope Header SwiftUI Block

A feed header: the title, scopes as text tabs with a sliding underline, filter chips that scroll, and a Filters button with its count.

The header of a feed. Scopes such as For you, Following and Saved sit as text tabs with an underline that slides to the one you pick, and filter chips scroll sideways beside a Filters button that counts what is on. It pins over the feed as a section header and shows a hairline once it is stuck.

  • SwiftUI
  • iOS 17.0+
  • scope-header
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.

  • Scopes as text tabs with an underline that slides between them
  • Filter chips that scroll sideways and toggle with a check
  • A Filters button whose count rolls as filters change
  • Pins as a section header and shows a hairline when stuck
  • Bindings for the scope and the chips, with loading chips

02

Implementation notes

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

The title sits in the brand's display face over the feed's scopes ("For you", "Following", "Saved"), text tabs on a hairline with an underline that slides to the one you pick; a scope can show how many items are new. Under them a row of filter chips scrolls sideways: a chip on fills in textPrimary with a check, and the Filters button at the end of the row shows how many filters are on, rolling to each new count. Your app owns the scope and the chips through two bindings and reloads the feed when they change. The header brings its own ground so it can pin as a section header, and a hairline appears under it once it is stuck over the feed. Pass nil chips while they load and the row shows grey chips in their place.

Notes

  • ScopeHeader(title:scopes:scope:chips:selectedChips:filterCount:isStuck:onFilters:) takes the title, the scopes, a binding to the selected scope's id, the chips and a binding to the set of chips that are on.
  • ScopeHeader.Scope has an id, a title and an optional count of new items, shown after the title in textTertiary. ScopeHeader.Chip has an id, a title and an optional SF Symbol. sampleScopes and sampleChips are the Reading tab's.
  • chips: [] hides the chip row; nil shows it loading.
  • filterCount is the figure on the Filters button. It defaults to the chips that are on; pass your own when the filter sheet holds more than the chips. onFilters opens your full filter sheet; nil hides the button.
  • Pinning: place the header in Section(header:) inside LazyVStack(pinnedViews: .sectionHeaders) and give the stack .coordinateSpace(ScopeHeader.feedSpace). The header then notices when it is stuck and shows its hairline. Or pass isStuck from your own scroll tracking.
  • Scopes that don't fit the width (large text sizes) scroll sideways with the chips' behaviour.
  • Picking a scope and toggling a chip each tick with a selection haptic.
  • VoiceOver reads the title as a header, each scope as a button with Selected on the current one and "4 new" as its value, each chip with Selected when on and a hint, and the Filters button as "Filters, 2 on". Every control has a 44 point target.
  • Reduce Motion turns the underline slide and chip changes into short fades.
  • ScopeHeaderExample at the end of ScopeHeader.swift is the Reading tab in the preview: the header pinned over a feed of articles that follows the scope and the chips. Pass wrapsInNavigationStack: false to push it in your own stack.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • The title is the one display moment: the brand's display face (type.face) at 28 points, type.displayWeight capped at semibold, in textPrimary. When the face isn't bundled it falls back to SF in type.figureDesign, or in type.textDesign when figures are monospaced.
  • Scopes are type.body: semibold textPrimary when selected, regular textSecondary otherwise, with a 2 point textPrimary underline that moves with matchedGeometryEffect and motion.hero. An SPHairline runs under them.
  • Chips are type.subhead medium on fill with a 1 point separator stroke; a chip that is on is textPrimary with its label in bg.
  • The Filters button is fill with a separator ring; its count is the accent with accentOn figures in type.caption, ringed in bg: the block's only accent.
  • The header's ground is bg, so the feed can run under it. Chip changes use motion.snappy, adapted for Reduce Motion.