Swift-ios-skills swift-charts
Implement, review, or improve data visualizations using Swift Charts. Use when building bar, line, area, point, pie, or donut charts; when adding chart selection, scrolling, or annotations; when plotting functions with vectorized BarPlot, LinePlot, AreaPlot, or PointPlot; when customizing axes, scales, legends, or foregroundStyle grouping; or when creating specialized visualizations like heat maps, Gantt charts, stacked/grouped bars, sparklines, or threshold lines.
git clone https://github.com/dpearson2699/swift-ios-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/dpearson2699/swift-ios-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/swift-charts" ~/.claude/skills/dpearson2699-swift-ios-skills-swift-charts && rm -rf "$T"
skills/swift-charts/SKILL.mdSwift Charts
Build data visualizations with Swift Charts targeting iOS 26+. Compose marks inside a
Chart container, configure axes and scales with view modifiers, and
use vectorized plots for large datasets.
See references/charts-patterns.md for extended patterns, accessibility, and theming guidance.
Contents
- Workflow
- Chart Container
- Mark Types
- Axis Customization
- Scale Configuration
- Foreground Style and Encoding
- Selection (iOS 17+)
- Scrollable Charts (iOS 17+)
- Annotations
- Legend
- Vectorized Plots (iOS 18+)
- Common Mistakes
- Review Checklist
- References
Workflow
1. Build a new chart
- Define data as an
struct or useIdentifiable
key path.id: - Choose mark type(s):
,BarMark
,LineMark
,PointMark
,AreaMark
,RuleMark
, orRectangleMark
.SectorMark - Wrap marks in a
container.Chart - Encode visual channels:
,.foregroundStyle(by:)
,.symbol(by:)
..lineStyle(by:) - Configure axes with
/.chartXAxis
..chartYAxis - Set scale domains with
/.chartXScale(domain:)
..chartYScale(domain:) - Add selection, scrolling, or annotations as needed.
- For 1000+ data points, use vectorized plots (
,BarPlot
, etc.).LinePlot
2. Review existing chart code
Run through the Review Checklist at the end of this file.
Chart Container
// Data-driven init (single-series) Chart(sales) { item in BarMark(x: .value("Month", item.month), y: .value("Revenue", item.revenue)) } // Content closure init (multi-series, mixed marks) Chart { ForEach(seriesA) { item in LineMark(x: .value("Date", item.date), y: .value("Value", item.value)) .foregroundStyle(.blue) } RuleMark(y: .value("Target", 500)) .foregroundStyle(.red) } // Custom ID key path Chart(data, id: \.category) { item in BarMark(x: .value("Category", item.category), y: .value("Count", item.count)) }
Mark Types
BarMark (iOS 16+)
// Vertical bar BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales)) // Stacked by category (automatic when same x maps to multiple bars) BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales)) .foregroundStyle(by: .value("Product", item.product)) // Horizontal bar BarMark(x: .value("Sales", item.sales), y: .value("Month", item.month)) // Interval bar (Gantt chart) BarMark( xStart: .value("Start", item.start), xEnd: .value("End", item.end), y: .value("Task", item.task) )
LineMark (iOS 16+)
// Single line LineMark(x: .value("Date", item.date), y: .value("Price", item.price)) // Multi-series via foregroundStyle encoding LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp)) .foregroundStyle(by: .value("City", item.city)) .interpolationMethod(.catmullRom) // Multi-series with explicit series parameter LineMark( x: .value("Date", item.date), y: .value("Price", item.price), series: .value("Ticker", item.ticker) )
PointMark (iOS 16+)
PointMark(x: .value("Height", item.height), y: .value("Weight", item.weight)) .foregroundStyle(by: .value("Species", item.species)) .symbol(by: .value("Species", item.species)) .symbolSize(100)
AreaMark (iOS 16+)
// Stacked area AreaMark(x: .value("Date", item.date), y: .value("Sales", item.sales)) .foregroundStyle(by: .value("Category", item.category)) // Range band AreaMark( x: .value("Date", item.date), yStart: .value("Min", item.min), yEnd: .value("Max", item.max) ) .opacity(0.3)
RuleMark (iOS 16+)
RuleMark(y: .value("Target", 9000)) .foregroundStyle(.red) .lineStyle(StrokeStyle(dash: [5, 3])) .annotation(position: .top, alignment: .leading) { Text("Target").font(.caption).foregroundStyle(.red) }
RectangleMark (iOS 16+)
RectangleMark(x: .value("Hour", item.hour), y: .value("Day", item.day)) .foregroundStyle(by: .value("Intensity", item.intensity))
SectorMark (iOS 17+)
// Pie chart Chart(data, id: \.name) { item in SectorMark(angle: .value("Sales", item.sales)) .foregroundStyle(by: .value("Category", item.name)) } // Donut chart Chart(data, id: \.name) { item in SectorMark( angle: .value("Sales", item.sales), innerRadius: .ratio(0.618), outerRadius: .inset(10), angularInset: 1 ) .cornerRadius(4) .foregroundStyle(by: .value("Category", item.name)) }
Axis Customization
// Hide axes .chartXAxis(.hidden) .chartYAxis(.hidden) // Custom axis content .chartXAxis { AxisMarks(values: .stride(by: .month)) { value in AxisGridLine() AxisTick() AxisValueLabel(format: .dateTime.month(.abbreviated)) } } // Multiple AxisMarks compositions (different intervals for grid vs. labels) .chartXAxis { AxisMarks(values: .stride(by: .day)) { _ in AxisGridLine() } AxisMarks(values: .stride(by: .week)) { _ in AxisTick() AxisValueLabel(format: .dateTime.week()) } } // Axis labels (titles) .chartXAxisLabel("Time", position: .bottom, alignment: .center) .chartYAxisLabel("Revenue ($)", position: .leading, alignment: .center)
Scale Configuration
.chartYScale(domain: 0...100) // Explicit numeric domain .chartYScale(domain: .automatic(includesZero: true)) // Include zero .chartYScale(domain: 1...10000, type: .log) // Logarithmic scale .chartXScale(domain: ["Mon", "Tue", "Wed", "Thu"]) // Categorical ordering
Foreground Style and Encoding
BarMark(...).foregroundStyle(.blue) // Static color BarMark(...).foregroundStyle(by: .value("Category", item.category)) // Data encoding AreaMark(...).foregroundStyle( // Gradient .linearGradient(colors: [.blue, .cyan], startPoint: .bottom, endPoint: .top) )
Selection (iOS 17+)
@State private var selectedDate: Date? @State private var selectedRange: ClosedRange<Date>? @State private var selectedAngle: String? // Point selection Chart(data) { item in LineMark(x: .value("Date", item.date), y: .value("Value", item.value)) } .chartXSelection(value: $selectedDate) // Range selection .chartXSelection(range: $selectedRange) // Angular selection (pie/donut) .chartAngleSelection(value: $selectedAngle)
Scrollable Charts (iOS 17+)
Chart(dailyData) { item in BarMark(x: .value("Date", item.date, unit: .day), y: .value("Steps", item.steps)) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 3600 * 24 * 7) // 7 days visible .chartScrollPosition(initialX: latestDate) .chartScrollTargetBehavior( .valueAligned(matching: DateComponents(hour: 0), majorAlignment: .page) )
Annotations
BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales)) .annotation(position: .top, alignment: .center, spacing: 4) { Text("\(item.sales, format: .number)").font(.caption2) } // Overflow resolution .annotation( position: .top, overflowResolution: .init(x: .fit(to: .chart), y: .padScale) ) { Text("Label") }
Legend
.chartLegend(.hidden) // Hide .chartLegend(position: .bottom, alignment: .center, spacing: 10) // Position .chartLegend(position: .bottom) { // Custom HStack(spacing: 16) { ForEach(categories, id: \.self) { cat in Label(cat, systemImage: "circle.fill").font(.caption) } } }
Vectorized Plots (iOS 18+)
Use for large datasets (1000+ points). Accept entire collections or functions.
// Data-driven Chart { BarPlot(sales, x: .value("Month", \.month), y: .value("Revenue", \.revenue)) .foregroundStyle(\.barColor) } // Function plotting: y = f(x) Chart { LinePlot(x: "x", y: "y", domain: -5...5) { x in sin(x) } } // Parametric: (x, y) = f(t) Chart { LinePlot(x: "x", y: "y", t: "t", domain: 0...(2 * .pi)) { t in (x: cos(t), y: sin(t)) } }
Apply KeyPath-based modifiers before simple-value modifiers:
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y)) .foregroundStyle(\.color) // KeyPath first .opacity(0.8) // Value modifier second
Common Mistakes
1. Using ObservableObject instead of @Observable
// WRONG class ChartModel: ObservableObject { @Published var data: [Sale] = [] } struct ChartView: View { @StateObject private var model = ChartModel() } // CORRECT @Observable class ChartModel { var data: [Sale] = [] } struct ChartView: View { @State private var model = ChartModel() }
2. Missing series parameter for multi-line charts
// WRONG -- all points connect into one line Chart { ForEach(allCities) { item in LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp)) } } // CORRECT -- separate lines per city Chart { ForEach(allCities) { item in LineMark(x: .value("Date", item.date), y: .value("Temp", item.temp)) .foregroundStyle(by: .value("City", item.city)) } }
3. Too many SectorMark slices
// WRONG -- 20 tiny sectors are unreadable Chart(twentyCategories, id: \.name) { item in SectorMark(angle: .value("Value", item.value)) } // CORRECT -- group into top 5 + "Other" Chart(groupedData, id: \.name) { item in SectorMark(angle: .value("Value", item.value)) .foregroundStyle(by: .value("Category", item.name)) }
4. Missing scale domain when zero-baseline matters
// WRONG -- axis starts at ~95; small changes look dramatic Chart(data) { LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score)) } // CORRECT -- explicit domain for honest representation Chart(data) { LineMark(x: .value("Day", $0.day), y: .value("Score", $0.score)) } .chartYScale(domain: 0...100)
5. Static foregroundStyle overriding data encoding
// WRONG -- static color overrides by-value encoding BarMark(x: .value("X", item.x), y: .value("Y", item.y)) .foregroundStyle(by: .value("Category", item.category)) .foregroundStyle(.blue) // CORRECT -- use only the data encoding BarMark(x: .value("X", item.x), y: .value("Y", item.y)) .foregroundStyle(by: .value("Category", item.category))
6. Individual marks for 10,000+ data points
// WRONG -- creates 10,000 mark views; slow Chart(largeDataset) { item in PointMark(x: .value("X", item.x), y: .value("Y", item.y)) } // CORRECT -- vectorized plot (iOS 18+) Chart { PointPlot(largeDataset, x: .value("X", \.x), y: .value("Y", \.y)) }
7. Fixed chart height breaking Dynamic Type
// WRONG -- clips axis labels at large text sizes Chart(data) { ... } .frame(height: 200) // CORRECT -- adaptive sizing Chart(data) { ... } .frame(minHeight: 200, maxHeight: 400)
8. KeyPath modifier after value modifier on vectorized plots
// WRONG -- compiler error BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y)) .opacity(0.8) .foregroundStyle(\.color) // CORRECT -- KeyPath modifiers first BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y)) .foregroundStyle(\.color) .opacity(0.8)
9. Missing accessibility labels
// WRONG -- VoiceOver users get no context Chart(data) { BarMark(x: .value("Month", $0.month), y: .value("Sales", $0.sales)) } // CORRECT -- add per-mark accessibility Chart(data) { item in BarMark(x: .value("Month", item.month), y: .value("Sales", item.sales)) .accessibilityLabel("\(item.month)") .accessibilityValue("\(item.sales) units sold") }
Review Checklist
- Data model uses
or chart usesIdentifiable
key pathid: - Model uses
with@Observable
, not@StateObservableObject - Mark type matches goal (bar=comparison, line=trend, sector=proportion)
- Multi-series lines use
parameter orseries:.foregroundStyle(by:) - Axes configured with appropriate labels, ticks, and grid lines
- Scale domain set explicitly when zero-baseline matters
- Pie/donut limited to 5-7 sectors; small values grouped into "Other"
- Selection binding type matches axis data type (
for date axis)Date? - Scrollable charts set
for viewport.chartXVisibleDomain(length:) - Vectorized plots used for datasets exceeding 1000 points
- KeyPath modifiers applied before value modifiers on vectorized plots
- Accessibility labels added to marks for VoiceOver
- Chart tested with Dynamic Type and Dark Mode
- Legend visible and positioned, or intentionally hidden
- Ensure chart data model types are Sendable; update chart data on @MainActor
References
- Extended patterns: references/charts-patterns.md
- Apple docs: Swift Charts
- Apple docs: Creating a chart using Swift Charts