FioriSwiftUICore provides a set of timeline components for rendering chronological sequences of events: Timeline for full-detail items, TimelinePreviewItem for compact preview rows, and TimelineNowIndicator to mark the current time in the sequence.
Timeline
Timeline is a single timeline event item. It renders a vertical line and a timeline node on the axis, a timestamp stack to the left of the axis, and a main content stack (title, subtitle, attribute, status) to the right. A divider separates each item in a list.
Key parameters
title
any View / AttributedString
required
The event title.
timelineNode
any View / TimelineNodeType
required
The node shape rendered on the timeline axis. Use TimelineNodeType values: .open, .inProgress, .complete, .opaqueCircle, .transparentCircle.
timestamp
any View / AttributedString?
Primary timestamp displayed to the left of the axis.
Secondary timestamp (text or icon) shown below the primary timestamp.
subtitle
any View / AttributedString?
Subtitle text below the title.
attribute
any View / AttributedString?
Attribute string displayed beside or below the title.
Secondary status label or icon.
subAttribute
any View / AttributedString?
Secondary attribute string.
When true, the item is rendered with a dimmed past-event appearance.
When true, the item is highlighted as the current event.
Optional icon displayed inside or beside the node.
// A completed past event
Timeline(
timestamp: "06/21/24",
secondaryTimestamp: .icon(Image(systemName: "sun.max")),
timelineNode: .complete,
title: "Order confirmed",
subtitle: "PO-20240621-0042",
attribute: "Warehouse A",
status: .text("Done"),
isPast: true
)
// An in-progress event
Timeline(
timestamp: "06/22/24",
timelineNode: .inProgress,
title: "Shipped",
subtitle: "DHL Express",
attribute: "Tracking: 1Z999AA10123456784",
status: .text("In transit"),
isPresent: true
)
// A future open event
Timeline(
timestamp: "06/24/24",
timelineNode: .open,
title: "Estimated delivery",
status: .text("Pending")
)
Removing list separators
SwiftUI List adds a separator between every row by default. Apply .listRowSeparator(.hidden) to each Timeline item to remove the duplicate lines.
List(events) { event in
Timeline(
timestamp: event.date,
timelineNode: event.nodeType,
title: AttributedString(event.title),
isPast: event.isPast
)
.listRowSeparator(.hidden)
}
TimelinePreviewItem
TimelinePreviewItem is a compact version of Timeline designed for use in preview cards or summaries. It shows only a title, a node, and an optional timestamp and icon.
Key parameters
title
any View / AttributedString
required
The event title.
timelineNode
any View / TimelineNodeType
required
The node type for this item.
Duplicate of timelineNode carried as a value for layout calculations.
timestamp
any View / AttributedString?
Optional timestamp label.
Optional icon beside the node.
When true, renders the item with a future-event style.
VStack(alignment: .leading, spacing: 0) {
TimelinePreviewItem(
title: "Order confirmed",
timelineNode: .complete,
timestamp: "Jun 21",
nodeType: .complete
)
TimelinePreviewItem(
title: "Shipped",
timelineNode: .inProgress,
timestamp: "Jun 22",
nodeType: .inProgress
)
TimelinePreviewItem(
title: "Delivery",
timelineNode: .open,
timestamp: "Jun 24",
nodeType: .open,
isFuture: true
)
}
TimelineNowIndicator
TimelineNowIndicator renders a horizontal marker that can be placed between Timeline items to show the current point in time. It uses NowIndicatorNode as its axis marker.
List {
ForEach(pastEvents) { event in
Timeline(
timestamp: event.date,
timelineNode: .complete,
title: AttributedString(event.title),
isPast: true
)
.listRowSeparator(.hidden)
}
// Current time marker
TimelineNowIndicator()
.listRowSeparator(.hidden)
ForEach(futureEvents) { event in
Timeline(
timestamp: event.date,
timelineNode: .open,
title: AttributedString(event.title)
)
.listRowSeparator(.hidden)
}
}
Complete example: delivery timeline
struct DeliveryTimelineView: View {
let shipment: Shipment
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 0) {
ForEach(shipment.events) { event in
Timeline(
timestamp: event.formattedDate,
secondaryTimestamp: .text(event.formattedTime),
timelineNode: event.nodeType,
title: AttributedString(event.title),
subtitle: event.location.map { AttributedString($0) },
status: event.statusLabel.map { .text(AttributedString($0)) },
isPast: event.isCompleted,
isPresent: event.isCurrent
)
}
}
.padding(.horizontal)
}
}
}