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:
- 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-chartsor implement LTTB inline. - Switch BarMark to RectangleMark. RectangleMark renders faster because it skips the BarMark layout pass.
- Constrain visible domain. Combine
chartScrollableAxes(.horizontal)withchartXVisibleDomainto 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
.accessibilityLabelon the entire Chart to describe the chart purpose. - Add
.accessibilityChartDescriptoron 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
- SwiftUI Lists Mastery for the data tables that often pair with charts.
- RevenueCat in SwiftUI for the MRR data source behind the example chart.
- SwiftUI Performance Tips for tuning chart render time.