NewThe Flutter Kit — Flutter boilerplate$149$69
Tutorial

SwiftUI Charts in 2026: Build Production Quality Bar, Line & Area Charts

The complete 2026 SwiftUI Charts tutorial. Bar, line, area, combined charts, interactive selection, gestures, scrollable axes, and 10,000 data point performance. Working code for every common pattern.

Ahmed GaganAhmed Gagan
13 min read

Skip 100+ hours of setup. Get The Swift Kit $149 $99 one-time

Get it now →

The 30-second answer

Swift Charts ships with iOS 16 plus and provides seven primitive marks: BarMark, LineMark, AreaMark, PointMark, RuleMark, RectangleMark, and SectorMark. Wrap your data in a Chart with a ForEach, choose marks, and apply axis modifiers. For interactive charts, bind chartXSelection on iOS 17 plus or use the new chartGesture modifier on iOS 26. For large data sets, downsample to 500 visible points and use chartScrollableAxes.

This guide covers Swift Charts from first chart to production deployment in 2026. The seven mark types, six chart styles you will actually ship (bar, line, area, scatter, combo, sector), interactive selection, gestures and zoom, performance with thousands of points, and accessibility. Each pattern includes working code.

Setup and First Chart

Swift Charts is part of the Charts framework that ships with iOS 16. No package installation needed.

import Charts
import SwiftUI

struct DailySale: Identifiable {
    let id = UUID()
    let day: String
    let revenue: Double
}

struct SalesChart: View {
    let data: [DailySale]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("Day", item.day),
                y: .value("Revenue", item.revenue)
            )
            .foregroundStyle(.blue.gradient)
        }
        .frame(height: 240)
    }
}

Three things SwiftUI handles automatically: axis ticks, legend rendering, and accessibility. Each bar is announced by VoiceOver with the day and revenue values without any extra code.

Bar Charts: Stacked, Grouped, Normalized

struct CategorySale: Identifiable {
    let id = UUID()
    let day: String
    let category: String
    let revenue: Double
}

// Stacked
Chart(data) { item in
    BarMark(
        x: .value("Day", item.day),
        y: .value("Revenue", item.revenue)
    )
    .foregroundStyle(by: .value("Category", item.category))
}

// Grouped (side by side)
Chart(data) { item in
    BarMark(
        x: .value("Day", item.day),
        y: .value("Revenue", item.revenue)
    )
    .foregroundStyle(by: .value("Category", item.category))
    .position(by: .value("Category", item.category))
}

// Normalized (100 percent stacked)
Chart(data) { item in
    BarMark(
        x: .value("Day", item.day),
        y: .value("Revenue", item.revenue),
        stacking: .normalized
    )
    .foregroundStyle(by: .value("Category", item.category))
}

Horizontal Bar Chart

Swap the x and y arguments to flip orientation:

Chart(data) { item in
    BarMark(
        x: .value("Revenue", item.revenue),
        y: .value("Day", item.day)
    )
    .foregroundStyle(.green.gradient)
}

Line Charts with Smooth Interpolation

Chart(data) { item in
    LineMark(
        x: .value("Date", item.date),
        y: .value("MRR", item.mrr)
    )
    .interpolationMethod(.catmullRom)
    .lineStyle(StrokeStyle(lineWidth: 3))
    .foregroundStyle(.blue.gradient)
    .symbol(.circle)
    .symbolSize(40)
}
.frame(height: 220)

Interpolation options:

  • .linear: straight line segments (default).
  • .catmullRom: smooth curves through every point.
  • .cardinal: tighter curves with sharp peaks.
  • .monotone: smooth curves that never overshoot data values.
  • .stepStart, .stepCenter, .stepEnd: stair step lines.

Multiple Lines on One Chart

Chart {
    ForEach(mrrData) { point in
        LineMark(
            x: .value("Date", point.date),
            y: .value("Value", point.mrr)
        )
        .foregroundStyle(by: .value("Series", "MRR"))
    }

    ForEach(churnData) { point in
        LineMark(
            x: .value("Date", point.date),
            y: .value("Value", point.churn)
        )
        .foregroundStyle(by: .value("Series", "Churn"))
    }
}
.chartForegroundStyleScale([
    "MRR": Color.blue,
    "Churn": Color.red
])

Area Charts with Gradient Fill

Chart(data) { item in
    AreaMark(
        x: .value("Date", item.date),
        y: .value("Subscribers", item.count)
    )
    .interpolationMethod(.catmullRom)
    .foregroundStyle(
        LinearGradient(
            colors: [.blue.opacity(0.6), .blue.opacity(0.05)],
            startPoint: .top,
            endPoint: .bottom
        )
    )

    LineMark(
        x: .value("Date", item.date),
        y: .value("Subscribers", item.count)
    )
    .interpolationMethod(.catmullRom)
    .foregroundStyle(.blue)
    .lineStyle(StrokeStyle(lineWidth: 2))
}

Layer an AreaMark below a LineMark for the App Store style metrics chart. Use the same x and y values on both. The result is a filled area with a crisp line on top.

Combo Charts (Bar plus Line)

Chart {
    ForEach(data) { point in
        BarMark(
            x: .value("Day", point.day),
            y: .value("Sales", point.sales)
        )
        .foregroundStyle(.blue.gradient)

        LineMark(
            x: .value("Day", point.day),
            y: .value("Target", point.target)
        )
        .foregroundStyle(.orange)
        .lineStyle(StrokeStyle(lineWidth: 2, dash: [6, 4]))
    }
}
.chartYAxis {
    AxisMarks(position: .leading)
}

Interactivity: chartXSelection (iOS 17 plus)

@State private var selectedDate: Date?

Chart(data) { item in
    LineMark(
        x: .value("Date", item.date),
        y: .value("Revenue", item.revenue)
    )

    if let selected = selectedDate,
       let point = data.first(where: { Calendar.current.isDate($0.date, inSameDayAs: selected) }) {
        RuleMark(x: .value("Selected", selected))
            .foregroundStyle(.gray.opacity(0.5))
            .annotation(position: .top, alignment: .center) {
                VStack(alignment: .leading) {
                    Text(selected, format: .dateTime.month().day())
                        .font(.caption2).foregroundStyle(.secondary)
                    Text(point.revenue, format: .currency(code: "USD"))
                        .font(.headline.bold())
                }
                .padding(8)
                .background(.regularMaterial, in: .rect(cornerRadius: 8))
            }
    }
}
.chartXSelection(value: $selectedDate)

The user taps or drags on the chart, the binding updates with the closest x value, and your annotation appears. iOS 17 made interactive chart selection a one liner.

iOS 26: chartGesture

iOS 26 added the chartGesture modifier for fully custom gestures including pinch to zoom, pan, and arbitrary overlays.

@State private var xDomain: ClosedRange<Date> = ...

Chart(data) { ... }
    .chartXScale(domain: xDomain)
    .chartGesture { proxy in
        MagnificationGesture()
            .onChanged { scale in
                let center = xDomain.midpoint
                let span = xDomain.span / scale
                xDomain = (center - span / 2)...(center + span / 2)
            }
    }

Scrollable Chart for Long Data Series

Chart(data) { item in
    BarMark(
        x: .value("Day", item.date, unit: .day),
        y: .value("Sales", item.sales)
    )
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 3600 * 24 * 14) // 14 days
.chartScrollPosition(x: $scrollX)

The chart renders the full data set but only 14 days are visible at a time. The user scrolls horizontally to see the rest. chartScrollPosition binds the scroll x to a date so you can programmatically scroll to today, or react to user scroll for analytics.

Performance with Large Data Sets

Swift Charts handles 5,000 to 10,000 simple marks (BarMark, LineMark) smoothly on modern iPhones. Above that, three optimizations apply:

  1. Downsample with LTTB. The Largest Triangle Three Buckets algorithm reduces a series to roughly 500 visually faithful points. Use a Swift package like swift-collections-charts or implement LTTB inline.
  2. Switch BarMark to RectangleMark. RectangleMark renders faster because it skips the BarMark layout pass.
  3. Constrain visible domain. Combine chartScrollableAxes(.horizontal) with chartXVisibleDomain to render only the visible window. The full data stays accessible via scroll.
let downsampled = LTTB.downsample(data, threshold: 500)

Chart(downsampled) { item in
    LineMark(
        x: .value("Time", item.timestamp),
        y: .value("Value", item.value)
    )
}

Real World Example: RevenueCat Webhook Powered MRR Chart

Combine the patterns above into a real chart you might ship in an indie iOS app. Pull MRR data from your RevenueCat webhook handler and render with selection.

struct MRRChart: View {
    let data: [MRRSnapshot]
    @State private var selected: Date?

    var body: some View {
        Chart {
            ForEach(data) { snap in
                AreaMark(
                    x: .value("Date", snap.date),
                    y: .value("MRR", snap.mrr)
                )
                .interpolationMethod(.catmullRom)
                .foregroundStyle(
                    LinearGradient(
                        colors: [.green.opacity(0.6), .green.opacity(0.05)],
                        startPoint: .top, endPoint: .bottom
                    )
                )

                LineMark(
                    x: .value("Date", snap.date),
                    y: .value("MRR", snap.mrr)
                )
                .interpolationMethod(.catmullRom)
                .foregroundStyle(.green)
                .lineStyle(StrokeStyle(lineWidth: 2.5))
            }

            if let selected, let snap = data.first(where: { Calendar.current.isDate($0.date, inSameDayAs: selected) }) {
                RuleMark(x: .value("Selected", selected))
                    .foregroundStyle(.gray.opacity(0.4))
                    .annotation(position: .top, alignment: .center) {
                        VStack(alignment: .leading) {
                            Text(selected, format: .dateTime.month().day())
                                .font(.caption2).foregroundStyle(.secondary)
                            Text(snap.mrr, format: .currency(code: "USD"))
                                .font(.headline.bold())
                        }
                        .padding(8)
                        .background(.regularMaterial, in: .rect(cornerRadius: 8))
                    }
            }
        }
        .chartXSelection(value: $selected)
        .chartYAxis {
            AxisMarks(position: .leading) { value in
                AxisGridLine()
                AxisValueLabel(format: .currency(code: "USD"))
            }
        }
        .chartXAxis {
            AxisMarks(values: .stride(by: .month)) { value in
                AxisValueLabel(format: .dateTime.month(.abbreviated))
            }
        }
        .frame(height: 280)
    }
}

Custom Axis Marks

Chart(data) { item in
    BarMark(...)
}
.chartYAxis {
    AxisMarks(position: .leading, values: [0, 500, 1000, 2000]) { value in
        AxisGridLine().foregroundStyle(.gray.opacity(0.2))
        AxisValueLabel {
            if let intValue = value.as(Int.self) {
                Text("\(intValue)").font(.caption)
            }
        }
    }
}
.chartXAxis {
    AxisMarks(values: .stride(by: .day)) { value in
        AxisValueLabel(format: .dateTime.weekday(.narrow), centered: true)
    }
}

Accessibility on Swift Charts

  • VoiceOver reads each mark with its x and y values automatically.
  • Use .accessibilityLabel on the entire Chart to describe the chart purpose.
  • Add .accessibilityChartDescriptor on iOS 17 plus for richer descriptions and AudioGraph support.
  • Test with VoiceOver. Swift Charts respects Reduce Motion automatically by skipping animations.

Swift Charts vs Custom Path

When to skip Swift Charts and draw with Path:

  • Radar charts, sankey diagrams, force directed graphs.
  • Per stroke animations (drawing strokes one at a time with timing).
  • Specialized scientific or financial visualizations not covered by the seven primitive marks.
  • Brand specific motion that requires custom interpolation timing.

For 95 percent of dashboards, analytics, and app metric charts, Swift Charts is the right tool. It is faster to ship, accessible by default, and integrates with the system look.

The Swift Kit Includes Charts Patterns

The Swift Kit ships chart patterns wired to the design system tokens: an MRR chart for subscription apps, a daily activity chart for habit apps, a goal progress chart for fitness apps, and a comparison chart for analytics dashboards. Plug in your data, change the color via a single token, and you have a polished chart that matches the rest of your app.

Frequently Asked Questions

Does Swift Charts work on iOS 15?

No. Swift Charts requires iOS 16 minimum. For iOS 15 support, use community packages like AAInfographics or build a Path based renderer for the specific chart types you need.

Can I export a Swift Chart as an image?

Yes. Wrap the Chart view in ImageRenderer on iOS 16 plus. Returns a UIImage you can save or share.

How do I make a Swift Chart respect dark mode?

By default Swift Charts uses semantic colors that adapt to dark mode. Avoid hard coded colors like .black in foreground styles. Use .primary, .secondary, or design system tokens that have light and dark variants.

Can Swift Charts render real time updating data?

Yes. Drive the data array from an @Observable view model. When the array updates, Swift Charts animates the transition with the default transition curve. Use .transition(.identity) on individual marks if you want to disable per mark animation.

Where to Go Next

Share this article
Limited-time · price rises to $149 soon

Ready to ship your iOS app faster?

The Swift Kit gives you a production-ready SwiftUI codebase with onboarding, paywalls, auth, AI integrations, and a full design system. Stop rebuilding boilerplate — start building your product.

$149$99one-time · save $50
  • Full source code
  • Unlimited projects
  • Lifetime updates
  • 50+ makers shipping