How to Build a Chart in SwiftUI
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
-
Chart { } closure — The
Chartview accepts a result-builder closure. EveryBarMark,LineMark, orRuleMarkyou place inside becomes a layer in the same coordinate space, so the targetRuleMarkand the data marks share the same y-axis scale automatically. -
PlottableValue —
.value("Label", item.revenue)wraps your data in a typedPlottableValue. The label becomes the axis title and the legend key; the generic type (StringvsDouble) tells the framework whether the axis is categorical or quantitative. -
chartOverlay + proxy.value(atX:) —
chartOverlaygives you aChartProxythat 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. -
chartXAxis / chartYAxis — These modifiers replace the default axis configuration. Passing
AxisMarks(format:)with aFormatStyle(e.g.,.currency(code: "USD")) applies consistent number formatting across all tick labels without a manualForEach. -
animation(_, value:) — Attaching
.animation(.easeInOut, value: chartStyle)to theChartcauses SwiftUI to interpolate between bar and line geometries wheneverchartStylechanges, 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
- iOS 16 vs 17 axis API.
chartXAxisandAxisMarks(format:)with aFormatStylewere refined in iOS 16, butchartOverlay'sproxy.plotFrameproperty is iOS 17+. Guard with#available(iOS 17, *)if you still target iOS 16. - Mixed axis types crash at runtime. If one
BarMarkuses aStringx-value and another uses anInt, Charts will throw a runtime assertion. Keep all marks on the same axis using the same Swift type — use a computed property to normalise data before rendering. - Accessibility. Charts auto-generates an Audio Graph (shake the device or use the Accessibility shortcut) but the default labels are generic. Add
.accessibilityLabel()to each mark — e.g.,.accessibilityLabel("\(item.month), \(item.revenue, format: .currency(code: "USD"))")— so VoiceOver reads meaningful values.
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.