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

How to implement a line chart in SwiftUI

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

Use Apple's Charts framework with LineMark(x:y:) inside a Chart view to render a line chart in SwiftUI. iOS 16+ supports Charts, but iOS 17 adds chart selection, scrolling, and chartXSelection modifier — target iOS 17+ for the full feature set.

import Charts
import SwiftUI

struct SalesPoint: Identifiable {
    let id = UUID()
    let day: Date
    let revenue: Double
}

struct MiniLineChart: View {
    let data: [SalesPoint]

    var body: some View {
        Chart(data) { point in
            LineMark(
                x: .value("Day", point.day),
                y: .value("Revenue", point.revenue)
            )
            .interpolationMethod(.catmullRom)
            .foregroundStyle(.indigo)
        }
        .frame(height: 200)
    }
}

Full implementation

The example below builds a polished line chart with a gradient area fill, interactive selection using chartXSelection, a custom rule mark to highlight the selected value, and axis formatting. All features require iOS 17+. The chart is driven by a simple @State data array so you can swap in your own model layer without changes.

import Charts
import SwiftUI

// MARK: – Model

struct RevenuePoint: Identifiable {
    let id = UUID()
    let date: Date
    let value: Double
}

// MARK: – Sample data helper

private func makeData() -> [RevenuePoint] {
    let calendar = Calendar.current
    let base = calendar.startOfDay(for: Date())
    return (0..<14).map { offset in
        let date = calendar.date(byAdding: .day, value: offset, to: base)!
        let value = Double.random(in: 1_200...8_500)
        return RevenuePoint(date: date, value: value)
    }
}

// MARK: – Chart view

struct LineChartView: View {
    let data: [RevenuePoint]

    // iOS 17 chart selection
    @State private var selectedDate: Date?

    private var selectedPoint: RevenuePoint? {
        guard let selectedDate else { return nil }
        return data.min(by: {
            abs($0.date.timeIntervalSince(selectedDate)) <
            abs($1.date.timeIntervalSince(selectedDate))
        })
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Header
            Text("Daily Revenue")
                .font(.title3.bold())
            if let pt = selectedPoint {
                Text(pt.value, format: .currency(code: "USD"))
                    .font(.headline)
                    .foregroundStyle(.indigo)
                    .contentTransition(.numericText())
                    .animation(.easeOut(duration: 0.15), value: pt.value)
            } else {
                Text("Tap to inspect")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            // Chart
            Chart(data) { point in
                // Gradient area fill
                AreaMark(
                    x: .value("Date", point.date),
                    y: .value("Revenue", point.value)
                )
                .interpolationMethod(.catmullRom)
                .foregroundStyle(
                    LinearGradient(
                        colors: [.indigo.opacity(0.3), .indigo.opacity(0)],
                        startPoint: .top, endPoint: .bottom
                    )
                )

                // Main line
                LineMark(
                    x: .value("Date", point.date),
                    y: .value("Revenue", point.value)
                )
                .interpolationMethod(.catmullRom)
                .foregroundStyle(.indigo)
                .lineStyle(StrokeStyle(lineWidth: 2.5))

                // Selection dot
                if let sel = selectedPoint, sel.id == point.id {
                    PointMark(
                        x: .value("Date", point.date),
                        y: .value("Revenue", point.value)
                    )
                    .symbolSize(80)
                    .foregroundStyle(.indigo)
                    .annotation(position: .top) {
                        Text(point.value, format: .currency(code: "USD"))
                            .font(.caption.bold())
                            .padding(6)
                            .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8))
                    }
                }
            }
            // iOS 17: declarative x-axis selection
            .chartXSelection(value: $selectedDate)
            .chartYAxis {
                AxisMarks(position: .leading) { value in
                    AxisValueLabel {
                        if let v = value.as(Double.self) {
                            Text(v / 1_000, format: .number.precision(.fractionLength(0)))
                                .font(.caption)
                            + Text("k").font(.caption)
                        }
                    }
                    AxisGridLine(stroke: StrokeStyle(lineWidth: 0.5, dash: [4]))
                        .foregroundStyle(.secondary.opacity(0.4))
                }
            }
            .chartXAxis {
                AxisMarks(values: .stride(by: .day, count: 3)) { value in
                    AxisValueLabel(format: .dateTime.day().month(.abbreviated))
                        .font(.caption)
                    AxisGridLine().foregroundStyle(.clear)
                }
            }
            .frame(height: 240)
            .padding(.top, 4)
        }
        .padding()
        .background(.background, in: RoundedRectangle(cornerRadius: 20))
        .shadow(color: .black.opacity(0.06), radius: 12, y: 4)
    }
}

// MARK: – Preview

#Preview {
    LineChartView(data: makeData())
        .padding()
        .preferredColorScheme(.light)
}

How it works

  1. LineMark + AreaMark layering. Both marks share the same x:/y: plottable values and the same .interpolationMethod(.catmullRom) — this keeps the smooth curve in sync between the fill and the stroke, so they never diverge.
  2. .chartXSelection(value: $selectedDate). This iOS 17 modifier writes the nearest x-axis date into selectedDate as the user drags a finger. It replaces the older chartOverlay + DragGesture workaround and handles accessibility automatically.
  3. Computed selectedPoint. Because the chart can return any interpolated date, we find the closest real data point using min(by:) on the absolute time interval difference. This ensures the dot always snaps to an actual data node.
  4. Conditional PointMark annotation. The annotation block is only emitted when sel.id == point.id, so only one tooltip is rendered at a time. SwiftUI's .contentTransition(.numericText()) animates the currency value smoothly as the user scrubs.
  5. Custom axis formatters. chartYAxis divides raw dollar values by 1,000 and appends "k", keeping labels compact. chartXAxis strides by every third day to avoid label crowding on a 14-day range.

Variants

Multi-series line chart

Add a series dimension to your model and pass it to .foregroundStyle(by:). The Charts framework automatically assigns distinct colors and generates a legend.

struct MetricPoint: Identifiable {
    let id = UUID()
    let date: Date
    let value: Double
    let series: String   // e.g. "iOS", "Android"
}

struct MultiSeriesChart: View {
    let data: [MetricPoint]

    var body: some View {
        Chart(data) { point in
            LineMark(
                x: .value("Date", point.date),
                y: .value("Value", point.value)
            )
            .foregroundStyle(by: .value("Platform", point.series))
            .interpolationMethod(.monotone)
        }
        .chartLegend(position: .top, alignment: .trailing)
        .frame(height: 220)
    }
}

#Preview {
    let today = Date()
    let cal = Calendar.current
    let data: [MetricPoint] = (0..<7).flatMap { offset -> [MetricPoint] in
        let d = cal.date(byAdding: .day, value: offset, to: today)!
        return [
            MetricPoint(date: d, value: .random(in: 500...3000), series: "iOS"),
            MetricPoint(date: d, value: .random(in: 300...2000), series: "Android")
        ]
    }
    return MultiSeriesChart(data: data).padding()
}

Scrollable / large-range chart

For datasets longer than 30 days, wrap the chart in .chartScrollableAxes(.horizontal) (iOS 17+) and set a visible domain with .chartXVisibleDomain(length: 60 * 60 * 24 * 14) (14 days in seconds). This lets users swipe to explore historical data while keeping the visible window readable. Pair with .chartScrollPosition(x: $scrollAnchor) to programmatically jump to a date.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement a line chart in SwiftUI for iOS 17+.
Use Charts/LineMark with .interpolationMethod(.catmullRom).
Include an AreaMark gradient fill beneath the line.
Add chartXSelection for interactive tap/drag value inspection.
Make it accessible (VoiceOver labels on Chart and each LineMark).
Add a #Preview with realistic 14-day revenue sample data.

In the Soarias Build phase, paste this prompt into the active session after your data model is scaffolded — Claude Code will wire the chart into your existing SwiftData or @Observable store without disrupting surrounding views.

Related

FAQ

Does this work on iOS 16?

LineMark and the core Chart view are available from iOS 16. However, .chartXSelection, .chartScrollableAxes, and .chartScrollPosition are iOS 17-only. If you need iOS 16 support, remove those modifiers and implement selection manually via chartOverlay with a DragGesture.

How do I animate the line drawing on appear?

Apply .chartYScale(domain: 0...maxValue) combined with an .animation(.easeOut, value: data.count) on the Chart itself. For a true draw-on animation (line grows from left to right), trim the data array using a timer-driven @State index and append points one by one in an .onAppear — Charts will animate the transition automatically because each LineMark is identified by its data point's Identifiable id.

What is the UIKit equivalent?

In UIKit you would use Swift Charts via a hosting controller (Charts is a SwiftUI-first framework with no UIKit-native API), or reach for a third-party library such as DGCharts (formerly Charts / Daniel Gindi). For sparklines only, CAShapeLayer with a UIBezierPath drawn through your data points is a lightweight alternative. In 2026, the recommended path for new UIKit projects is to embed a UIHostingController containing the SwiftUI Chart view.

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

```