Block · Productivity
NewStats 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
$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 oneStatsOverviewBlock.Overview?. It is a section: no navigation stack, background, padding or scroll view of its own, so place it in your own column: aScrollViewwith.padding(.horizontal, 20), or a sheet. A trailing closure isonRetry.Overviewholds the headline metric'slabel("Revenue"), itsformat,periods(aPeriodperTimeRange), fallbackmetricsfor periods without their own, andincreaseIsGood(false when a fall is good news).Overview.sampleis the shop from the preview: revenue over all four ranges, seeded, dated up to today.- A
Periodis one range's numbers:points(oldest first), thepreviousperiod's points (empty hides the dashed line), the headlinevalueat rest (nil uses the latest point, so pass the range's total for summed metrics), thechangeas a fraction (0.126 reads +12.6%; nil shows the range in words, "Last 30 days") and the range's tilemetrics. Points may be hourly, daily, weekly or monthly; the block reads their spacing to word a scrubbed date. Pointis adateand avalue.Metrichas anid(defaults to the label),label,value,format, an optionalchangeandincreaseIsGood.Formatwrites every figure:.currency("USD")($44,051.40, orfractionDigits: 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.TimeRangeis.week,.month,.quarterand.year, titled 1W, 1M, 3M and 1Y, each with itscomparison("vs last month") andspan("Last 30 days").rangespicks which the control offers, in order.- Pass
rangeas a binding when your app owns the selection; without it the block keeps its own, starting on 1M.onRangeChangeruns on every pick: fetch that range'sPeriodthere and leave it out ofperiodsuntil it arrives, and the block shows ghost shapes for it meanwhile. errorreplaces the numbers with "Couldn't load your numbers" and the error'serrorDescription(or "Check your connection and try again.").onRetryisasync throws: the Secondary button shows a spinner until it returns, then a checkmark; clearerrorand pass the new overview when it succeeds. A thrown error shows its message under the button in the danger colour;CancellationErrorgoes back to rest quietly. Nil hides the button.onSelectMetricruns with the tappedMetric. 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.
StatsOverviewBlockExampleat the end ofStatsOverviewBlock.swiftis 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; passwrapsInNavigationStack: falseto push it in your own stack.
Theming
- Reads
SPThemefrom@Environment(\.spTheme)and requires thedesign-systemitem. - The card and the tiles are
color.surfacewithradius.lgand 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)attype.numeralWeight, capped at semibold) withtype.tracking(for:)and monospaced digits, intextPrimary. Words never use the display face. The metric's name and the change line usetype.subheadmedium; tile labelstype.subhead; tile valuestype.display(22)capped at semibold with monospaced digits; tile changestype.footnotemedium; chart datestype.caption. - Text is
textPrimaryandtextSecondary; chart dates and tile chevrons aretextTertiary. Changes aresuccessordanger, ortextSecondarywhen flat. - The line is the
accentat 2 points, the one highlighted series; the previous period istextTertiaryat 60% in a 2 on, 4 off dash. The scrub rule istextTertiary, the dot theaccentringed insurface. - The range control's selection is a
fillplate withradius.smbehindtextPrimary; the other ranges aretextSecondary. - Ghost shapes are
fill. Try again is the Secondary button:fill,textPrimaryintype.headline,radius.md, 52 points tall; its error line istype.footnoteindanger. - Range changes, phases and tile values move with
motion.smooth; the headline value and Try again's states withmotion.snappy. Every animation becomes a short fade under Reduce Motion.
03
Keep building
Pieces that work well next to this one.