Skip to content

Native Task Manager

iOS index · Web TaskGantt contract

Available in the SwiftUI integration for iOS 26+. TaskManagerView currently renders an adaptive hierarchical list. TaskTimelineView adds a read-only civil-day chart with optional consumer actions.

Data and ownership

ChronoSketchCore exports GanttTask, GanttTaskKind, TaskSchedule, TaskCollection, TaskRow and TaskGanttError. Activities have an optional civil-date schedule and progress in 0–100 (use zero for omitted Web progress). Intervals have exclusive ends. Milestones have one date and summaries derive bounds and arithmetic mean progress from all descendant activities, including unscheduled and collapsed ones. Milestones contribute bounds, not progress. TaskRow.lastDate is the inclusive last occupied date for list labels, including milestone points; start/end retain the Web aggregation contract.

swift
import ChronoSketchCore
import ChronoSketchUI

let task = try GanttTask(
  id: "design", title: "Design",
  kind: .activity(
    schedule: TaskSchedule(start: CivilDate("2026-09-08"), end: CivilDate("2026-09-10")),
    progress: 25))
let data = try TaskCollection([task])

TaskManagerView(data: data, language: .english,
                actions: [.details, .schedule, .duration, .progress]) { action, task in
  // Open your own form. Keep a draft and persist before replacing confirmed data.
}

The consumer supplies data as a SwiftUI value. Confirm with try data.replacing(previous: original, proposed: updated) and assign the result to your own observed state. This rejects stale/removed originals and validates the entire candidate collection before returning a replacement. Validation rejects duplicate/blank identities, invalid progress, nonpositive intervals, invalid parents and cyclic hierarchy. rows(collapsed:) is a pure projection. Each view owns only its own collapsed IDs and open popover.

Touch and accessibility

Tap requests .details when enabled. Long press opens a stationary anchored popover with shared activation haptics. VoiceOver exposes the same actions. The actions set defaults to details only. Missing capabilities stay hidden: summary rows allow details/group/order changes; milestones allow details/dates/group/order changes; unscheduled activities allow details/dates/progress, but no duration until scheduled.

There are no small drag/resize/progress grips. The demo offers date-only pickers, duration in whole days, progress slider/stepper and explicit Save/Close. Closing discards the draft. Moving preserves duration; changing duration retains start; changing progress retains dates. Success feedback follows confirmation. The package neither persists changes nor ships these demo forms.

Remaining TaskGantt parity

Native lists and time axes support actions, navigation, zoom, dependencies and decorative media and a code-configured component title. There is no separate task status model. Web retains its existing date/progress/reorder gestures. Shared task semantics and eventual capability parity remain the target; this list is not full TaskGantt parity.

Time axis

swift
try TaskTimelineView(
  data: data,
  start: try CivilDate("2026-09-08"), days: 90,
  language: .english)

The consumer supplies a finite civil-date horizon with an exclusive end. At default zoom one day occupies 72 points. Activities render their exact exclusive interval; summary bars use derived bounds, and milestone diamonds sit within their date column. Unscheduled tasks keep their row with an Unscheduled label. Progress is read-only and clipped using the original task interval. Tasks outside the horizon keep their name but have no visible bar. The view owns disclosure, scroll and the active zoom.

The task-name column and date header remain pinned while the chart scrolls. Content uses the shared strict scroll-boundary adapter. Pass actions and onAction to enable the same tap/long-press forms as the list. There are no direct resize or progress grips. Axis labels support German/English.

The demo initially shows the list on compact iPhone layouts and the chart with names alongside it on regular iPad layouts. Both offer List/Timeline selection. The demo horizon is 90 days from its anchor date. Navigation stays within this fixed horizon. The original axis and dependency arrows were accepted on device; no complete Web parity or large-horizon performance claim is made.

Finish-to-start dependencies

TaskCollection accepts optional dependencies: [TaskDependency] (default empty). Each immutable relationship names its predecessorID and successorID and always means finish-to-start, matching the Web type: 'finish-to-start' contract.

swift
let linked = try TaskCollection(tasks, dependencies: [
  TaskDependency(predecessorID: "design", successorID: "build"),
  TaskDependency(predecessorID: "build", successorID: "release")
])

The complete collection must contain both endpoints. Activities (including unscheduled ones) and milestones are valid endpoints; summaries are not. Missing endpoints, self-links, duplicate links and dependency cycles throw before creating a collection. replacing(previous:proposed:) retains and revalidates existing dependencies. When constructing an entirely new collection yourself, pass the dependencies you want to retain explicitly.

The chart draws directional orthogonal arrows between visible scheduled rows. Collapsing a summary hides its descendants' arrows. Unscheduled endpoints retain their data relationships but have no arrow until scheduled. Original endpoints are used before clipping to the horizon and pinned task-column boundary; horizontal scrolling therefore cannot draw lines over task labels. Milestone connections meet the displayed diamond. Successor tracks expose predecessor names to accessibility services; the decorative drawing has no hit targets.

These are visual relationships only: dates may overlap or run in reverse order. Saving an activity never shifts successors, and arrows are recalculated from confirmed dates. There is no automatic scheduling, dependency editor, constraint validation or other link type. The demo keeps two clear links: Design → Implementation → Release. The user accepted dependency display on device.

Time-axis zoom

TaskTimelineView accepts zoom: TimelineZoom = .oneDay and showZoom: Bool = true. The same values as Web are supported: .oneDay, .twoDays, .fourDays, .sixDays, .eightDays (1/2/4/6/8 civil days per 72-point column). Changing the supplied zoom updates the existing view; the built-in large minus/ plus buttons also maintain local zoom. Unchanged consumer input does not reset that local selection. showZoom: false hides controls without changing the scale.

swift
TaskTimelineView(data: data, range: horizon, language: .english,
                 zoom: .twoDays, showZoom: true)

At the left edge, zoom keeps the horizon start visible. After scrolling right, zoom preserves the civil time at the center of the visible chart area, excluding the pinned task names. At finite horizon edges the offset is clamped; a horizon narrower than the viewport stays left-aligned. No extra dates are loaded or added. Columns label inclusive date ranges; the last column is clipped to the exclusive horizon end. Activities, progress, summaries and dependency endpoints use the same day ratio. Milestone diamonds retain their readable symbol size. Zoom never changes dates, progress, hierarchy or dependencies. List form editing still uses individual civil days. The zoom behavior has been accepted on device.

Task zoom keeps the horizon start anchored when the viewport is at its left edge (including when the entire horizon fits). Zooming back in therefore keeps early tasks in view. After scrolling right, zoom retains the viewport center as before. This rule applies to native and Web TaskGantt; it does not reposition task dates or guarantee that every task fits at the most detailed zoom.

Canonical scroll configuration is try TaskTimelineView(data: data, start: start, days: 90). Days must be positive; unsupported date bounds throw. Internally the end remains exclusive. The older initializer accepting range: TaskSchedule remains a compatibility path. A consumer may also create the resolved range with try TaskSchedule(start: start, days: 90), as the demo does. Event schedules still support explicit ends; this migration concerns scroll configuration only.

Creating tasks

Both TaskManagerView and TaskTimelineView accept optional onCreate: ((TaskCreateRequest) -> Void)? (default nil). When present, a large Add task button requests creation with parentID == nil. In the list, a summary's stationary long-press menu also offers Add child task, supplying its ID. VoiceOver exposes the same action. Omit the callback to hide creation controls.

swift
TaskManagerView(data: data, language: .english,
  onCreate: { request in
    // Open your own draft/form; request.parentID is an optional suggested group.
  },
  onAction: { action, task in /* Existing details/edit actions. */ })

The library does not insert a temporary task or allocate an ID. Your application creates the GanttTask and confirms via try data.adding(task), assigning the returned collection to its observed state after validation/persistence. This preserves existing dependencies and rejects duplicate IDs, missing/non-summary parents and invalid task values. Closing your form requires no library mutation. Summaries are still derived; adding a child recomputes their bounds/progress.

The demo supports activities (scheduled or unscheduled), milestones and groups, root or nested under an existing group. It assigns a stable draft UUID, trims titles, and uses civil date/duration controls. Save is disabled for empty titles. Invalid confirmation leaves the form open and confirmed data unchanged. Success/failure use the existing semantic haptics. If sample visibility was off, saving makes the resulting collection visible. User-created titles survive demo language changes. Deletion and dependency editing are separate follow-ups.

The task chart stays top-aligned when rows collapse or fit inside its viewport. Its frame surrounds the date header and scroll content; collapsing removes rows below the group without vertically recentering the header or remaining group.

Changing a task's group

Opt into TaskAction.group in TaskManagerView.actions to offer Change group in its stationary long-press menu and accessibility actions. It is available for activities, milestones and summaries; the default action set remains details only. Your onAction callback receives the unchanged task. Open a consumer-owned parent picker and confirm with data.replacing(previous:proposed:). Construct the proposed GanttTask with its original ID, title and kind and the selected parentID (nil for root level). Consumers with exhaustive switches over TaskAction must handle the new .group case.

The demo displays large parent-selection rows directly in the group-edit form. It excludes the task itself and all descendants, including collapsed groups. Closing discards the draft; Save validates current confirmed data and provides success feedback. Missing/non-summary parents, cycles and stale originals are rejected without changing confirmed data. Moving a summary carries its subtree; other tasks' parent IDs, dates, progress and dependency links remain unchanged. Summary bounds/progress are recalculated. Input order continues to determine sibling order; this is not sibling reordering. A task moved into a collapsed group is hidden until that group is expanded. The timeline reflects confirmed hierarchy changes and offers the same consumer forms.

Changing sibling order

Include .order in TaskManagerView.actions to expose Change order through the existing action callback, long-press menu and VoiceOver actions. The default stays [.details]. All task kinds support order changes. Consumers with exhaustive TaskAction switches must handle the new .order case.

Use a consumer-owned form to choose another task with the same parentID (including nil for roots) and a TaskOrderPosition.before or .after intention. Confirm with:

swift
let updated = try data.reordering(
  previous: originalTask, relativeTo: originalSibling, position: .after)
data = updated

The collection requires the captured source and target values to still exist unchanged; removed/edited values throw TaskGanttError.staleTask. Self or cross-parent targets throw .invalidHierarchy. Confirmed data is never changed on rejection. A changed sibling order alone does not invalidate a relative before/after intention. The method permutes only sibling slots in the input array, matching Web. Task content, parent IDs, descendant ordering, dates, progress and dependency links remain unchanged. Summary subtrees move together even when collapsed; the timeline reprojects rows and arrows. Adjacent no-op requests return equal data.

The demo uses large inline choices, Before/After controls and Save/Close. Closing leaves order unchanged. With no sibling target, it explains why ordering is unavailable and disables Save. Successful changes use existing success haptics; no-op ordering stays silent and failures keep the form open. Editing forms can be requested from the list or chart; the chart has no direct manipulation. Web retains its drag/keyboard ordering contract. The user confirmed this ordering slice on device.

Month and ISO-week headers

TaskTimelineView always shows month, ISO-week and day/zoom rows, as Web does. No additional option is required. All three rows stay above the tasks while scrolling vertically; month/week labels follow horizontal scrolling within their own section. Sections are clipped at the configured horizon and retain exact civil boundaries, even when a zoom column crosses a month or week boundary. German labels use KW, English uses Week; the ISO week-year is shown explicitly (for example January 1, 2021 belongs to week 53 of 2020). The chart header now occupies 116 points. The period headers have been accepted on device.

Period labels remain at the left edge across multiple columns until the next period pushes the text away. This uses actual label width, including localized text, rather than the viewport width.

Actions on the time axis

Pass actions: Set(TaskAction.allCases) and onAction to TaskTimelineView just as for TaskManagerView. The callback receives the original task. Both initializers accept these optional parameters; without a callback, the chart keeps its read-only behavior. Tap a task name or bar for details; long press opens the shared stationary action popover with activation haptics. VoiceOver exposes the same actions. Available actions depend on task kind/schedule. Summary actions also offer child creation when onCreate is supplied. Group disclosure remains separate. Tiny bars have a minimum 44-point hit area; unscheduled/off-range tasks remain reachable by their pinned names. The consumer owns Save/Cancel and data.

Finite navigation

TaskTimelineView shows Previous/Today/Next by default. Previous/Next move half of the component width, matching Web; they disable at the finite scroll edges. Today uses the device's current Gregorian civil date, or the optional injected today. It scrolls within the configured horizon, without extending it. Dates outside the horizon clamp to its nearest reachable edge. Optional date sets the initial position and can be updated by the consumer; nil uses horizon start. Changing the configured range reapplies date. showNavigation: false hides the buttons while preserving manual scrolling and programmatic date selection.

Colors, icons and avatars

Apply the same resolver to either TaskManagerView or TaskTimelineView (or their parent). The closure runs on the main actor, receives the original task and must be side-effect-free. Keep metadata in a consumer lookup keyed by task ID.

swift
TaskTimelineView(
  data: data, range: range, language: .english,
  actions: Set(TaskAction.allCases), onAction: openTaskForm,
  date: selectedDate, today: today)
.taskAppearance { task in
  TaskAppearance(
    tint: task.id == "design" ? .teal : nil,
    systemImage: "pencil",
    media: TimelineMedia(source: .asset("DesignerAvatar"), systemImage: "person.fill"))
}

TaskAppearance defaults to no decorations and the inherited tint. systemImage is an optional SF Symbol; media reuses the native TimelineMedia contract: asset/remote sources, avatar/logo variants, and symbol/initials loading/failure fallbacks. See media details. Icons and media can be supplied together. List progress and chart bars use tint; names retain normal text contrast. Decorations are hidden from accessibility; the task's complete title/dates remain the accessible content. The list and pinned name show media at 20 points. Activity bars show 18-point decorations only at widths of at least 60 points; shorter bars, summaries and milestones retain their geometry, with media available in their pinned names. Presentation changes never change task values, schedules, ordering or dependency links. Existing consumers need no modifier.

Component heading

Both native views accept title: String = "Task Manager", independently of task names. This is code configuration, not an editable control. SwiftUI updates the heading when the supplied value changes; it does not reset task data or selection.

swift
TaskManagerView(data: data, title: "Project planning", onAction: openTaskForm)
try TaskTimelineView(data: data, start: start, days: 90, title: "Project planning")

The heading supports multiline text and accessibility heading navigation.

Height modes

Both task surfaces support .heightMode(.fill) (default) and .heightMode(.content, maxHeight: 400). The limit is in points and applies to the scrolling chart/list; the heading and controls sit above it. The parent layout may offer less space. Without a maximum, content fits its rows up to the parent's available height; fill consumes that available height. Supply a bounded parent when using fill inside an otherwise unbounded vertical ScrollView.

The chart includes its pinned time header in the content height, adapts to collapsed groups and data changes, and draws no grid below the last row. Content list mode measures its rows, including Dynamic Type, using a plain scrolling stack; fill list mode retains the native List appearance. Both retain actions and consumer-owned data. Empty content keeps a readable localized message. Negative maximum heights clamp to zero; nonfinite limits are treated as absent.

Apply .heightMode(.fill, maxHeight: 400) to cap the allocated viewport, or .heightMode(.content, maxHeight: 400) to shrink it to visible rows within that limit. These modifiers work on both TaskManagerView and TaskTimelineView. The demo's shared settings select the mode; numeric point limits are configured in the embedding SwiftUI code.

ChronoSketch · Calendar and scheduling components