```html SwiftUI: How to Build a Chart (iOS 17+, 2026)

How to Build a Chart in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: Charts Updated: May 11, 2026
TL;DR

Import Charts, wrap your data array in a Chart view, and use BarMark, LineMark, or PointMark to render each data point. Axis labels and interactive selection are controlled with modifier chains.

import Charts
import SwiftUI

struct SalesData: Identifiable {
    let id = UUID()
    let month: String
    let revenue: Double
}

struct MiniChartView: View {
    let data: [SalesData]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("Month", item.month),
                y: .value("Revenue", item.revenue)
            )
            .foregroundStyle(.blue)
        }
        .frame(height: 200)
    }
}

Full implementation

The example below builds a polished sales dashboard with a segmented toggle that switches between bar and line chart styles. It uses @State to track the selected chart type, chartXAxis and chartYAxis modifiers to style the axes, and a RuleMark to draw a target line. A tap gesture on Chart uses chartOverlay to surface the nearest data point — a common pattern for drill-down interactions. Everything runs on iOS 17+ with no UIKit shim needed.

import Charts
import SwiftUI

// MARK: - Model

struct SalesPoint: Identifiable {
    let id = UUID()
    let month: String
    let revenue: Double
}

// MARK: - View

struct SalesDashboardView: View {
    let data: [SalesPoint]
    let target: Double = 45_000

    @State private var chartStyle: ChartStyle = .bar
    @State private var selectedMonth: String? = nil

    enum ChartStyle: String, CaseIterable, Identifiable {
        case bar = "Bar"
        case line = "Line"
        var id: String { rawValue }
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            // Header
            HStack {
                VStack(alignment: .leading, spacing: 2) {
                    Text("Monthly Revenue")
                        .font(.title2.bold())
                    Text("FY 2026")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
                Spacer()
                Picker("Chart style", selection: $chartStyle) {
                    ForEach(ChartStyle.allCases) { style in
                        Text(style.rawValue).tag(style)
                    }
                }
                .pickerStyle(.segmented)
                .frame(width: 130)
            }

            // Selected month callout
            if let month = selectedMonth,
               let point = data.first(where: { $0.month == month }) {
                HStack {
                    Image(systemName: "chart.bar.fill")
                        .foregroundStyle(.blue)
                    Text("\(month): ")
                        .fontWeight(.semibold)
                    + Text(point.revenue, format: .currency(code: "USD"))
                }
                .font(.subheadline)
                .padding(8)
                .background(.blue.opacity(0.08), in: RoundedRectangle(cornerRadius: 8))
            }

            // Chart
            Chart {
                // Target rule
                RuleMark(y: .value("Target", target))
                    .foregroundStyle(.orange.opacity(0.7))
                    .lineStyle(StrokeStyle(lineWidth: 1, dash: [6, 3]))
                    .annotation(position: .top, alignment: .trailing) {
                        Text("Target")
                            .font(.caption2)
                            .foregroundStyle(.orange)
                    }

                ForEach(data) { item in
                    if chartStyle == .bar {
                        BarMark(
                            x: .value("Month", item.month),
                            y: .value("Revenue", item.revenue)
                        )
                        .foregroundStyle(
                            item.month == selectedMonth
                                ? AnyShapeStyle(Color.blue)
                                : AnyShapeStyle(Color.blue.opacity(0.55))
                        )
                        .cornerRadius(4)
                    } else {
                        LineMark(
                            x: .value("Month", item.month),
                            y: .value("Revenue", item.revenue)
                        )
                        .foregroundStyle(.blue)
                        .interpolationMethod(.catmullRom)

                        PointMark(
                            x: .value("Month", item.month),
                            y: .value("Revenue", item.revenue)
                        )
                        .foregroundStyle(
                            item.month == selectedMonth ? .blue : .blue.opacity(0.5)
                        )
                        .symbolSize(item.month == selectedMonth ? 80 : 40)
                    }
                }
            }
            .chartXAxis {
                AxisMarks(values: .automatic) {
                    AxisValueLabel()
                        .font(.caption)
                }
            }
            .chartYAxis {
                AxisMarks(format: .currency(code: "USD").precision(.fractionLength(0)))
            }
            .chartOverlay { proxy in
                GeometryReader { geo in
                    Rectangle()
                        .fill(.clear)
                        .contentShape(Rectangle())
                        .onTapGesture { location in
                            let relX = location.x - geo[proxy.plotFrame!].origin.x
                            if let month: String = proxy.value(atX: relX) {
                                selectedMonth = (selectedMonth == month) ? nil : month
                            }
                        }
                }
            }
            .frame(height: 260)
            .animation(.easeInOut(duration: 0.3), value: chartStyle)
        }
        .padding()
        .background(.background)
        .clipShape(RoundedRectangle(cornerRadius: 16))
        .shadow(color: .black.opacity(0.06), radius: 12, y: 4)
    }
}

// MARK: - Preview

#Preview {
    SalesDashboardView(data: [
        SalesPoint(month: "Jan", revenue: 32_000),
        SalesPoint(month: "Feb", revenue: 27_500),
        SalesPoint(month: "Mar", revenue: 41_200),
        SalesPoint(month: "Apr", revenue: 38_800),
        SalesPoint(month: "May", revenue: 51_000),
        SalesPoint(month: "Jun", revenue: 46_300),
    ])
    .padding()
    .background(Color(.systemGroupedBackground))
}

How it works

  1. Chart { } closure — The Chart view accepts a result-builder closure. Every BarMark, LineMark, or RuleMark you place inside becomes a layer in the same coordinate space, so the target RuleMark and the data marks share the same y-axis scale automatically.
  2. PlottableValue.value("Label", item.revenue) wraps your data in a typed PlottableValue. The label becomes the axis title and the legend key; the generic type (String vs Double) tells the framework whether the axis is categorical or quantitative.
  3. chartOverlay + proxy.value(atX:)chartOverlay gives you a ChartProxy that maps between screen coordinates and data values. proxy.value(atX: relX) reverse-maps a tap X position to the nearest categorical string, enabling tap-to-select without manual hit-testing.
  4. chartXAxis / chartYAxis — These modifiers replace the default axis configuration. Passing AxisMarks(format:) with a FormatStyle (e.g., .currency(code: "USD")) applies consistent number formatting across all tick labels without a manual ForEach.
  5. animation(_, value:) — Attaching .animation(.easeInOut, value: chartStyle) to the Chart causes SwiftUI to interpolate between bar and line geometries whenever chartStyle changes, giving a smooth morph transition for free.

Variants

Area chart with gradient fill

Chart(data) { item in
    AreaMark(
        x: .value("Month", item.month),
        y: .value("Revenue", item.revenue)
    )
    .interpolationMethod(.catmullRom)
    .foregroundStyle(
        LinearGradient(
            colors: [.blue.opacity(0.35), .blue.opacity(0.05)],
            startPoint: .top,
            endPoint: .bottom
        )
    )

    LineMark(
        x: .value("Month", item.month),
        y: .value("Revenue", item.revenue)
    )
    .interpolationMethod(.catmullRom)
    .foregroundStyle(.blue)
    .lineStyle(StrokeStyle(lineWidth: 2))
}
.frame(height: 220)

Grouped / stacked bar chart

Pass a foregroundStyle keyed to a second categorical dimension (e.g., region) and Charts will automatically group or stack bars. Use .chartForegroundStyleScale(["North": .blue, "South": .green]) to pin specific colours to categories so the legend is stable as data changes.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement a chart in SwiftUI for iOS 17+.
Use Charts (BarMark, LineMark, PointMark, chartXAxis, chartYAxis, chartOverlay).
Make it accessible (VoiceOver labels on each mark, Audio Graph support).
Add a #Preview with realistic sample data (6 months of revenue figures).

In Soarias's Build phase, paste this prompt into the inline Claude Code panel alongside your SwiftData model so the generated chart wires directly to your live data store — no extra plumbing needed.

Related

FAQ

Does this work on iOS 16?

The Charts framework itself shipped in iOS 16, so BarMark, LineMark, and basic axis modifiers work there. However, chartOverlay's proxy.plotFrame and several AxisMarks(format:) overloads are iOS 17+. If you need iOS 16 support, guard the tap-to-select logic with #available(iOS 17, *) and fall back to a simpler gesture approach.

Can I animate individual bars when data updates?

Yes — because Chart is a standard SwiftUI view, it participates in the normal animation system. Wrap your data mutation in withAnimation(.spring), and SwiftUI will interpolate bar heights and line paths smoothly. For initial load animations, add .transition(.scale(scale: 0, anchor: .bottom)) to each mark and drive it with an onAppear state toggle.

What is the UIKit equivalent?

The closest UIKit equivalent is DGCharts (the community fork of Charts / iOS-Charts) or the now-unmaintained MPAndroidChart port. Apple's native Charts framework has no UIKit counterpart — it is SwiftUI-only. If you must embed it in a UIKit hierarchy, wrap SalesDashboardView in a UIHostingController.

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

```