Pattern · Lists and Feeds

New

Activity Timeline SwiftUI Block

A day-by-day log on a rail: status-tinted nodes, attachment chips, folded runs, a filter by kind and paging to earlier activity.

For order history, account activity or a project log. Events group under Today, Yesterday and dated headers, each with a node on a hairline rail that is tinted only for a real outcome, a title and time, a detail line and an optional attachment chip. Runs of one kind fold into one line that opens in place, a menu filters by kind, and Show earlier activity pages back with its working, failed and end states.

  • SwiftUI
  • iOS 17.0+
  • activity-timeline
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.

  • Day groups on a hairline rail with a node per event
  • Nodes tinted only for real outcomes, like a failed build
  • Attachment chips that open their file or link
  • Runs of one kind fold into "4 more edits"
  • A filter menu by kind and paging to earlier activity

02

Implementation notes

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

The timeline opens with "All activity" and a Filter menu that narrows it to one kind of event. Events group by day under Today, Yesterday and dated headers, each day on a surface card with a hairline rail down its leading edge. Every event has a 28 point node on the rail, its symbol in a neutral circle, or tinted by its status when the status is a real outcome: a deploy that succeeded, a build that failed. Beside the node sit the title and its time, an optional detail line, and an optional attachment chip that opens its file or link. Three or more events of one kind in a row fold under the first into one line ("4 more edits") with a small dot on the rail; tapping it opens the run in place and Show fewer folds it again. At the end, Show earlier activity runs your onLoadMore closure, shows a spinner with "Loading earlier activity", and the older events land as new days; a thrown error shows under the button, which becomes Try again. When there is nothing older, the log ends with "That's everything". A filter with nothing to show and an empty log each have their own message, and passing nil shows grey shapes while the log loads.

Notes

  • ActivityTimeline(events:kinds:hasMore:onLoadMore:onOpenAttachment:) takes the events loaded so far and the kinds that describe them. It is a section: no navigation stack, background, padding or scroll view of its own, so place it in your own ScrollView column.
  • ActivityTimeline.Kind has an id, a plural title for the menu ("Deploys"), the rail symbol, the unit a folded run is counted in ("edits", "updates" by default) and collapses, which turns folding off for kinds that should always show every event. ActivityTimeline.sampleKinds is a project's deploys, edits, comments, files and members.
  • ActivityTimeline.Event holds an id, its kind id, the title, an optional detail, the date, an optional symbol that overrides the kind's, an optional Status (.success, .warning, .danger, .info) and an optional Attachment (title, symbol, url). Leave status nil for events with no outcome, so the rail stays neutral. Event.samples and Event.earlierSamples are the two pages from the preview.
  • Events can arrive in any order: the block sorts them newest first and groups them by the calendar day.
  • Folding only joins events of one kind that sit next to each other in the same day, so the order of what happened is never changed.
  • hasMore shows Show earlier activity while onLoadMore is set; false shows the end of the log. onLoadMore is async throws: append the older page to events and update hasMore before it returns. A thrown error shows its errorDescription (or "Couldn't load earlier activity."); CancellationError returns to rest.
  • Attachment chips open url with openURL. Pass onOpenAttachment to handle them yourself, for example to open a file in your own viewer; then chips without a URL show too.
  • Haptics mark a selection when the filter changes, success when a page loads, and an error when it fails.
  • VoiceOver reads each event as its title, detail, outcome and time, folded runs as buttons with hints, and announces a loaded page. Titles and times stack at large text sizes, and the node keeps a readable size.
  • Reduce Motion turns every change into a short fade.
  • ActivityTimelineExample at the end of ActivityTimeline.swift is the Activity screen in the preview: two days of a website project with five edits folded under the first, and an earlier page that reaches the project's start. Pass wrapsInNavigationStack: false to push it in your own stack.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • Day cards are color.surface with radius.lg and 24 points between days (space.xl). Day headers are type.subhead semibold in textSecondary; the filter title is type.headline.
  • The rail is a 1 point line in separator. Nodes are 28 point circles on fill with textSecondary symbols, or a 14% tint of success, warning, danger or info with the symbol in that colour.
  • Titles use type.body, details type.subhead in textSecondary, times type.footnote in textTertiary with monospaced digits. Attachment chips are fill capsules with type.footnote text.
  • The Filter button is a surface capsule with a separator stroke, on fill while a filter is on. Show earlier activity 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.