How to implement a line chart in SwiftUI
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
-
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. -
.chartXSelection(value: $selectedDate). This iOS 17 modifier writes the nearest x-axis date intoselectedDateas the user drags a finger. It replaces the olderchartOverlay + DragGestureworkaround and handles accessibility automatically. -
Computed
selectedPoint. Because the chart can return any interpolated date, we find the closest real data point usingmin(by:)on the absolute time interval difference. This ensures the dot always snaps to an actual data node. -
Conditional
PointMarkannotation. The annotation block is only emitted whensel.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. -
Custom axis formatters.
chartYAxisdivides raw dollar values by 1,000 and appends "k", keeping labels compact.chartXAxisstrides 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
- Charts requires iOS 16, but
chartXSelectionis iOS 17 only. If you add.chartXSelectionwithout an availability guard the app will crash on iOS 16. Either set your deployment target to iOS 17+ or wrap the modifier inif #available(iOS 17, *). - Passing unsorted data produces a scrambled line.
LineMarkconnects points in the order they appear in your array — it does not sort automatically. Always sort your data array by the x-axis value before passing it toChart:data.sorted(by: { $0.date < $1.date }). - Missing accessibility label on the chart. The Charts framework generates a default accessibility description, but it is generic. Add
.accessibilityLabel("Daily revenue over the past 14 days")to theChartview and use.accessibilityValueon individual marks so VoiceOver can read individual data points meaningfully.
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.