```html How to Build a Gantt Chart App in SwiftUI (2026)

How to Build a Gantt Chart App in SwiftUI

A Gantt Chart app lets users plan projects visually — draggable task bars, dependency arrows, and a scrollable timeline rendered natively on iOS. It's aimed at freelancers, project leads, and small teams who want a lightweight alternative to desktop tools.

iOS 17+ · Xcode 16+ · SwiftUI Complexity: Advanced Estimated time: 2–4 weeks Updated: May 12, 2026

Prerequisites

Architecture overview

The app uses SwiftData for persistence of projects, tasks, and dependency edges. A TimelineViewModel translates date ranges into pixel offsets for the Canvas renderer. The main view pairs a sticky left column (task names) with a horizontally scrollable Canvas for bars. Drag gestures update task dates directly on the model, and StoreKit 2 gates advanced features behind a subscription paywall.

GanttApp/
├── Models/
│   ├── Project.swift          # @Model: name, startDate, tasks
│   ├── GanttTask.swift        # @Model: title, start, end, progress, dependencies
│   └── Dependency.swift       # @Model: from, to task IDs
├── ViewModels/
│   └── TimelineViewModel.swift # date→x offset, zoom, visible range
├── Views/
│   ├── GanttRootView.swift    # NavigationSplitView
│   ├── TimelineView.swift     # ScrollView + Canvas
│   ├── TaskRowView.swift      # left-column labels
│   └── SubscriptionGateView.swift
└── PrivacyInfo.xcprivacy
        

Step-by-step

1. Data model with SwiftData

Define your persistence layer so tasks, projects, and dependencies survive app restarts and drive the timeline renderer via reactive queries.

import SwiftData
import Foundation

@Model
final class Project {
    var name: String
    var startDate: Date
    @Relationship(deleteRule: .cascade) var tasks: [GanttTask]

    init(name: String, startDate: Date = .now) {
        self.name = name
        self.startDate = startDate
        self.tasks = []
    }
}

@Model
final class GanttTask {
    var title: String
    var startDate: Date
    var endDate: Date
    var progress: Double        // 0.0–1.0
    var dependsOnIDs: [PersistentIdentifier]

    init(title: String, start: Date, end: Date) {
        self.title = title
        self.startDate = start
        self.endDate = end
        self.progress = 0
        self.dependsOnIDs = []
    }

    var durationDays: Int {
        Calendar.current.dateComponents([.day], from: startDate, to: endDate).day ?? 1
    }
}

2. Core UI — scrollable timeline grid

Build the two-panel layout: a fixed task-name column on the left and a horizontally scrollable date grid on the right, sharing a synchronized vertical scroll position.

struct TimelineRootView: View {
    @State private var vm = TimelineViewModel()
    @Query var tasks: [GanttTask]

    var body: some View {
        HStack(spacing: 0) {
            // Fixed left column
            ScrollView(.vertical, showsIndicators: false) {
                LazyVStack(spacing: 0) {
                    ForEach(tasks) { task in
                        TaskLabelRow(task: task)
                            .frame(height: 44)
                    }
                }
            }
            .frame(width: 160)
            .background(Color(.systemGroupedBackground))

            Divider()

            // Scrollable canvas area
            ScrollView([.horizontal, .vertical], showsIndicators: true) {
                GanttCanvasView(tasks: tasks, vm: vm)
                    .frame(
                        width: vm.totalWidth,
                        height: CGFloat(tasks.count) * 44
                    )
            }
        }
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                ZoomControl(vm: vm)
            }
        }
    }
}

3. Project timeline visualization with Canvas

Use SwiftUI's Canvas to draw Gantt bars, progress fills, today-line, and dependency arrows in a single GPU-accelerated pass — critical for smooth scrolling with many tasks.

struct GanttCanvasView: View {
    let tasks: [GanttTask]
    let vm: TimelineViewModel

    var body: some View {
        Canvas { ctx, size in
            // Date column header lines
            for col in vm.visibleDayColumns {
                let x = vm.x(for: col.date)
                ctx.stroke(Path { p in
                    p.move(to: CGPoint(x: x, y: 0))
                    p.addLine(to: CGPoint(x: x, y: size.height))
                }, with: .color(.gray.opacity(0.15)))
            }

            // Gantt bars
            for (idx, task) in tasks.enumerated() {
                let y = CGFloat(idx) * 44 + 10
                let x0 = vm.x(for: task.startDate)
                let x1 = vm.x(for: task.endDate)
                let barRect = CGRect(x: x0, y: y, width: x1 - x0, height: 24)

                // Background bar
                ctx.fill(RoundedRectangle(cornerRadius: 5).path(in: barRect),
                         with: .color(.blue.opacity(0.25)))

                // Progress fill
                var fillRect = barRect
                fillRect.size.width = barRect.width * task.progress
                ctx.fill(RoundedRectangle(cornerRadius: 5).path(in: fillRect),
                         with: .color(.blue))
            }

            // Today line
            let todayX = vm.x(for: .now)
            ctx.stroke(Path { p in
                p.move(to: CGPoint(x: todayX, y: 0))
                p.addLine(to: CGPoint(x: todayX, y: size.height))
            }, with: .color(.red.opacity(0.6)), lineWidth: 1.5)
        }
        .gesture(
            DragGesture()
                .onChanged { vm.handlePan($0) }
        )
    }
}

4. Privacy Manifest setup

Add a PrivacyInfo.xcprivacy to declare any Required Reason API usage (e.g. NSFileSystemFreeSize, UserDefaults) — missing manifests cause automatic rejection since Xcode 15.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>NSPrivacyTracking</key>
  <false/>
  <key>NSPrivacyTrackingDomains</key>
  <array/>
  <key>NSPrivacyCollectedDataTypes</key>
  <array/>
  <key>NSPrivacyAccessedAPITypes</key>
  <array>
    <dict>
      <key>NSPrivacyAccessedAPIType</key>
      <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
      <key>NSPrivacyAccessedAPITypeReasons</key>
      <array>
        <string>CA92.1</string>
      </array>
    </dict>
  </array>
</dict>
</plist>

Common pitfalls

Adding monetization: Subscription

Use StoreKit 2's Product.products(for:) to fetch your monthly and annual subscription SKUs defined in App Store Connect, then gate advanced features — dependency linking, export to PDF, unlimited projects — behind a Transaction.currentEntitlement(for:) check. Present your paywall using a SubscriptionStoreView (iOS 17+), which handles introductory offers and family sharing automatically. Always verify receipts server-side for renewals and handle StoreKit.Transaction.updates in a background task so entitlement state stays accurate across sessions and device restores.

Shipping this faster with Soarias

Soarias scaffolds the entire SwiftData model layer, wires up a StoreKit 2 subscription paywall, and generates a correctly formed PrivacyInfo.xcprivacy from a single prompt — the three steps that typically consume a full weekend on an advanced project like this. It also sets up fastlane lanes for screenshot capture across six device sizes and handles the App Store Connect metadata submission, including age ratings and review notes.

For an advanced app of this complexity, most developers spend 1–2 weeks on boilerplate, CI wiring, and submission paperwork before touching the core Canvas renderer. Soarias compresses that overhead to a few hours, so you can direct the full 2–4 week budget toward the actual differentiator: the interactive Gantt timeline itself.

Related guides

FAQ

Do I need a paid Apple Developer account?

Yes. A free account lets you sideload onto your own device, but distributing on TestFlight or the App Store — and activating StoreKit subscriptions — requires the $99/year Apple Developer Program membership.

How do I submit this to the App Store?

Archive the app in Xcode (Product → Archive), upload via the Organizer or xcrun altool, then complete the App Store Connect listing — screenshots, description, privacy nutrition labels, and subscription pricing. Apple's review typically takes 1–3 business days for a first submission.

Can I export the Gantt chart to PDF or share it?

Yes — use ImageRenderer (iOS 16+) to rasterize your Canvas view into a UIImage, then wrap it in a PDFDocument via PDFKit. Gate this behind your subscription paywall as a premium export feature to add meaningful upgrade incentive.

Last reviewed: 2026-05-12 by the Soarias team.

```