Appearance
Gantt / resource Timeline for SwiftUI
iOS overview · Integration · Shared foundations
The SwiftUI integration also provides TimelineModel and TimelineView, separate from Calendar. The native Timeline displays seven zoomable columns with resources on the left and floating timed bars on the right. Resource labels and both time header rows stay fixed along their respective scroll axes. Overlaps use separate lanes. Horizontal scrolling is enabled only when the seven-column canvas exceeds the available width. Fitting content stays left-aligned. On iOS, both axes stop at their content boundaries without elastic overscroll, including when content overflows. This is shared by Hour, Day and Week. Scrolling overflowing content remains available.
swift
// In a throwing @MainActor setup function:
let resource = TimelineResource(id: "room", label: "Meeting room")
let entry = try TimelineEntry(
id: "booking", title: "Workshop",
start: LocalDateTime("2026-09-08T09:00"),
end: LocalDateTime("2026-09-10T17:00")
)
let assignment = TimelineAssignment(id: "booking-room", entryId: entry.id, resourceId: resource.id)
let data = try TimelineData(resources: [resource], entries: [entry], assignments: [assignment])
let timeline = try TimelineModel(start: CivilDate("2026-09-08"), data: data, days: 90)
// Retain timeline on the main actor, then use it in your SwiftUI view:
TimelineView(model: timeline, today: today) { entry, assignment in
// Both are original values, even if the visible bar was clipped.
}IDs must be nonempty and unique within each collection. Resource labels must be nonempty. Assignments must reference supplied resources and entries. One entry may have multiple assignments. TimelineData validates the complete snapshot; entry ends are exclusive and must follow their starts. No timezone conversion occurs.
Use try timeline.setData(data), timeline.setLanguage(.english), try timeline.select(date) and try timeline.navigate(forward: true) to synchronize consumer changes. With days, navigation/selection scroll within the fixed horizon. Invalid ranges preserve prior state. Dates/language/data are instance-owned; pass today's civil date explicitly.
The native demo's Calendar/Gantt switch retains each model and shares language, appearance and sample-data visibility. Large accessibility text sizes use a resource list with the same group controls. The Timeline has no resource reassignment, pinch zoom, edge autoscroll or task hierarchy. This is not full Web Timeline parity.
For explicit type annotations, use ChronoSketchUI.TimelineView; SwiftUI also defines an unrelated type named TimelineView.
Content, icons, avatars and actions
Use native content builders for entry, resource and group labels; appearance resolvers for bar colors, SF Symbols, avatars and logos; and consumer context actions alongside interval editing. See the presentation guide for complete contracts, examples, fallback behavior and accessibility.
Asynchronous data loading
An optional TimelineDataProvider loads the viewport's interval and resources. The view provides loading/error/retry states; stale or cancelled responses cannot replace newer confirmed data. See the provider integration guide for request/response contracts, bounded caching, lifecycle and edit freshness. The synchronous setData integration remains available.
Grouping and zoom
Supply string-valued resource groups and ordered grouping definitions:
swift
let rooms = [
TimelineResource(id: "room", label: "Meeting room",
groups: ["site": "Berlin", "building": "A"])
]
let groupedData = try TimelineData(resources: rooms, entries: [entry], assignments: [assignment])
try timeline.setData(groupedData)
try timeline.setGroupBy([
TimelineGrouping(key: "site", label: "Location"),
TimelineGrouping(key: "building", label: "Building")
])
try timeline.setZoom(.fourDays) // four days per column, 28-day windowGroups appear in first-seen resource order. Visible titles are group values; the label describes the dimension, matching Web's contract. Missing/empty values share a localized Ohne Zuordnung / Unassigned group. Headers show descendant resource counts. Resources stay in their supplied order inside each group. Grouping keys must be nonempty and unique; invalid updates leave state intact.
timeline.toggleGroup(["Berlin", "A"]) toggles a structured value path; unknown paths are ignored. setGroupsExpanded(false) collapses every level, including hidden/offscreen descendants; true opens all. Data updates preserve existing collapsed paths and discard vanished ones. Changing groupBy resets collapse; reapplying equal definitions preserves it. Language, navigation, zoom and switching the demo component retain grouping/collapse state. Groups cannot receive assignments.
TimelineZoom supports .oneDay (default), .twoDays, .fourDays, .sixDays and .eightDays; raw values 1/2/4/6/8 are multipliers of the selected scale. The day-named cases remain available for source compatibility; .twoDays means two hours per column in .hour, or two weeks per column in .week. Toolbar minus/plus buttons zoom out/in while text and resource-row sizes remain unchanged. Columns cover the configured horizon; the final column may be partial. days contains every occupied civil day. The legacy columnStarts contains civil dates, which repeat in hour scale; use window.columnStarts for distinct LocalDateTime anchors. rows retains all resource layouts; outline includes visible group/resource rows. collapsedGroups, groupBy and zoom are read-only model state controlled by the methods above.
With days, zoom retains the horizon and keeps the left edge anchored at its start; scrolled content uses center anchoring. The viewport clamps at the bounds. Entry intervals and callbacks remain exact, with exclusive ends. Invalid date bounds reject the entire zoom/navigation update, preserving previous state.
Scales and period headers
Day is the default. Select .hour, .day or .week in the component's segmented control, or use the model API:
swift
try timeline.setScale(.hour)
try timeline.select(LocalDateTime("2026-09-08T08:00"))
try timeline.setZoom(.twoDays) // 2 hours per column, a 14-hour window
try timeline.navigate(forward: true) // starts at 22:00, crosses midnight
// Optionally hide the extra overview row; the column header stays visible.
timeline.setShowPeriodHeader(false)The initializer also accepts scale and showPeriodHeader (default true). It accepts either a CivilDate (midnight) or a LocalDateTime. Hour preserves that exact time. Day/Week normalize to midnight on the same civil date; Week does not realign to Monday. Switching scale preserves the selected date, zoom multiplier, data and collapsed groups. Zoom preserves the exact start. Unsupported end dates reject the entire transition, leaving both the model and interaction revision intact.
window exposes the validated start, exclusive end, minutes, columnMinutes and columnStarts. Hour navigation advances by the window's exact minutes, even across midnight. start remains a civil-date convenience. Today and select(CivilDate) start at midnight; use select(LocalDateTime) for a daytime hour window. The maximum Week window is 392 days; all boundaries must remain within supported years. No time-zone or DST conversion occurs.
| Scale | Additional overview row | Edit step |
|---|---|---|
| Hour | Civil day and date | 15 minutes |
| Day | ISO week number and week-year, in both languages | 1 day |
| Week | Calendar month and year | 1 day |
Overview sections split at exact period boundaries, including inside a zoomed column. Both header rows stay pinned vertically, while resource labels stay pinned horizontally. The demo offers a Period header toggle and a short sample booking for hour-scale inspection. At accessibility text sizes the resource list uses the same selected time window and scale controls.
TimelineLayout.rows(data:window:) projects clipped minute offsets for any scale. The existing rows(data:start:days:) convenience retains its 1–56-day limit. TimelinePeriod.sections(in:) provides clipped overview sections without UI labels.
Selection, context menu and precise editing
Tap an entry to call onEntrySelected with its original full interval and assignment. Long press opens the anchored action popover directly, without a lifted preview using configured entryActions. Scrolling remains ordinary scrolling. There is no drag/resize mode, preview, grip or raster-step haptic. onEntryEditRequested has been removed from TimelineView; remove that argument from integrations.
Consumers can open their own form through selection or a context action. The validated TimelineEntryEditRequest(entry:assignment:start:end:) and TimelineData.applying(_:) helpers remain available for precise changes. Confirm with model.setData; shared assignments retain identity and stale snapshots are rejected. The demo's “Zeitraum ändern / Edit interval” form remains available.
The default demo contains four resources and three separate examples: one, two and three full days. All entries start/end at midnight. Launch with --demo-timeline-stress only when testing dense/overlapping fixtures.
Fixed horizontal horizon
Supply days: 90 to TimelineModel (CivilDate or LocalDateTime start). The length is positive whole calendar days; floating starts retain their clock time, including across scale changes. Zoom and Hour/Day/Week change column density only. Internal window.start and exclusive window.end stay fixed. Entries do not extend them. The demo uses 90 days with the existing small fixture dataset, leaving empty space.
Toolbar arrows scroll half the visible chart width, Today scrolls to the date clamped within the horizon. Programmatic select/navigate update selectedTime within these same bounds. New models are required for different start/days configuration. Visible buffered provider queries are clipped to the horizon, including the partial last column. Scrollable length does not imply loading every entry in that range.
Compatibility: calls omitting days retain the old seven-column moving window; the earlier 7–56-unit examples describe that legacy mode. New integrations must supply days. Window/columnStarts counts are no longer always seven in finite mode.
Period overview labels stay at the left edge until the next period pushes them away: day labels in Hour, ISO weeks in Day, and months in Week. The same rule applies at every zoom level and uses the actual localized text width.
Height modes
TimelineView(...).heightMode(.fill) is the default. Its grid retains the height set by gridSize(height:resourceWidth:) (540 points by default). Use .heightMode(.content) to fit the pinned header and visible resource/group rows, up to that grid height. Overlap lanes, data replacement and disclosure recalculate height. Both modes scroll internally when needed. Unused fill space is plain: column strokes stop at the final row. Empty data retains a header and empty label. The accessibility list uses measured content with the same height policy. Sizing belongs to ChronoSketchUI; data and selection are unchanged.
For example, apply .gridSize(height: 400, resourceWidth: 132) and .heightMode(.content) to your TimelineView to fit its rows within 400 points. Use .heightMode(.fill) with the same gridSize for a fixed viewport. These are consumer configuration options; the demo's shared settings select the mode but do not expose a numeric height editor.