Block · Productivity

New

Stats Overview SwiftUI Block

The top of a dashboard: a headline metric and its change, a chart you scrub across 1W to 1Y, and metric tiles.

The section at the top of an analytics or dashboard screen. One card holds the headline metric with its change against the previous period and a Swift Charts line you drag across to read any day, with the previous period dashed beneath and a 1W to 1Y control that morphs the line. Metric tiles sit below, and loading, empty and failed states are built in.

  • SwiftUI
  • iOS 17.0+
  • stats-overview
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.

  • A headline metric with its change against the previous period
  • Drag across the Swift Charts line to read any point, the headline following
  • 1W, 1M, 3M and 1Y, with the line morphing between ranges
  • Two to four metric tiles that open their metric
  • Ghost shapes while loading, an empty range, and Try again on failure

02

Implementation notes

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

One surface card leads with the headline metric: its name, its value for the chosen range as the block's one large figure, and the change against the previous period ("+13.0% vs last month") with an arrow, in the success colour when it is good news and the danger colour when it is not. Under it a Swift Charts line draws in from the leading edge when the numbers arrive, with the previous period as a faint dashed line laid over the same span and three dates beneath. Drag across the chart and a rule and dot snap to each reading while the headline rolls to that reading's value and its date ("Mon, Oct 12", or "Week of Sep 22" for weekly points), with a selection tick per point; let go and it rolls back to the total. The 1W / 1M / 3M / 1Y control at the foot of the card slides its selection and the line morphs into the new range as every figure rolls to its new value. Two to four metric tiles sit below, two to a row, each with its label, value and change; tapping one opens it when you pass onSelectMetric. Pass nil while the numbers load and the block shows ghost shapes in its own layout; a range you fetch on demand shows the same shapes until its numbers land. A range with no readings says "No data for this range yet." Pass an error and the block shows a calm message with Try again, which shows progress while your closure runs and puts a thrown error's message under the button.

Notes

  • StatsOverviewBlock(_:range:ranges:error:onRetry:onRangeChange:onSelectMetric:) takes one StatsOverviewBlock.Overview?. It is a section: no navigation stack, background, padding or scroll view of its own, so place it in your own column: a ScrollView with .padding(.horizontal, 20), or a sheet. A trailing closure is onRetry.
  • Overview holds the headline metric's label ("Revenue"), its format, periods (a Period per TimeRange), fallback metrics for periods without their own, and increaseIsGood (false when a fall is good news). Overview.sample is the shop from the preview: revenue over all four ranges, seeded, dated up to today.
  • A Period is one range's numbers: points (oldest first), the previous period's points (empty hides the dashed line), the headline value at rest (nil uses the latest point, so pass the range's total for summed metrics), the change as a fraction (0.126 reads +12.6%; nil shows the range in words, "Last 30 days") and the range's tile metrics. Points may be hourly, daily, weekly or monthly; the block reads their spacing to word a scrubbed date.
  • Point is a date and a value. Metric has an id (defaults to the label), label, value, format, an optional change and increaseIsGood.
  • Format writes every figure: .currency("USD") ($44,051.40, or fractionDigits: 0), .number (1,172) or .number(fractionDigits:), .percent (a fraction, 0.032 reads 3.2%), .compact (12.4K) and .duration (seconds, 4m 12s). format.formatted(_:) is there for your own views.
  • TimeRange is .week, .month, .quarter and .year, titled 1W, 1M, 3M and 1Y, each with its comparison ("vs last month") and span ("Last 30 days"). ranges picks which the control offers, in order.
  • Pass range as a binding when your app owns the selection; without it the block keeps its own, starting on 1M. onRangeChange runs on every pick: fetch that range's Period there and leave it out of periods until it arrives, and the block shows ghost shapes for it meanwhile.
  • error replaces the numbers with "Couldn't load your numbers" and the error's errorDescription (or "Check your connection and try again."). onRetry is async throws: the Secondary button shows a spinner until it returns, then a checkmark; clear error and pass the new overview when it succeeds. A thrown error shows its message under the button in the danger colour; CancellationError goes back to rest quietly. Nil hides the button.
  • onSelectMetric runs with the tapped Metric. Nil leaves the tiles static and drops their chevrons.
  • Haptics: a selection tick on each range change and each scrubbed reading, success when Try again works and an error when it fails.
  • VoiceOver reads the headline as a header with a sentence for its value ("$44,051.40, up 13.0% vs last month"), offers the chart's audio graph and data table through accessibilityChartDescriptor, reads each range as a button with its span and selected state, and each tile as its label with its value and change. The ghost layout reads "Loading Revenue". A failed retry is announced.
  • Dynamic Type: the tiles go to one column at accessibility sizes, the headline figure shrinks to fit its width rather than truncating, the chart's dates stop growing at the largest standard size and the range control at the second accessibility size. Every row keeps its shape down to 320 points.
  • Reduce Motion skips the draw-in and the morph's spring: changes become short fades and the ghost shapes hold still.
  • StatsOverviewBlockExample at the end of StatsOverviewBlock.swift is the Overview screen in the preview: a small shop's revenue for the last 30 days against last month, orders, average order, conversion and refund rate. The numbers load as the screen opens, the year is fetched the first time 1Y is picked, and a tile opens its metric. It reads the theme from the environment; pass wrapsInNavigationStack: false to push it in your own stack.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • The card and the tiles are color.surface with radius.lg and 16 point padding (space.lg), sharing one 12 point gutter (space.md) as a grid.
  • The headline value is the one display moment: the family's numeral at 40 points (type.display(40) at type.numeralWeight, capped at semibold) with type.tracking(for:) and monospaced digits, in textPrimary. Words never use the display face. The metric's name and the change line use type.subhead medium; tile labels type.subhead; tile values type.display(22) capped at semibold with monospaced digits; tile changes type.footnote medium; chart dates type.caption.
  • Text is textPrimary and textSecondary; chart dates and tile chevrons are textTertiary. Changes are success or danger, or textSecondary when flat.
  • The line is the accent at 2 points, the one highlighted series; the previous period is textTertiary at 60% in a 2 on, 4 off dash. The scrub rule is textTertiary, the dot the accent ringed in surface.
  • The range control's selection is a fill plate with radius.sm behind textPrimary; the other ranges are textSecondary.
  • Ghost shapes are fill. Try again is the Secondary button: fill, textPrimary in type.headline, radius.md, 52 points tall; its error line is type.footnote in danger.
  • Range changes, phases and tile values move with motion.smooth; the headline value and Try again's states with motion.snappy. Every animation becomes a short fade under Reduce Motion.