Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Extended patterns, accessibility guidance, and theming for Swift Charts on
iOS 26+. Import Charts in every file that uses these APIs.
import SwiftUI
import ChartsUse @Observable for chart data models. Pair with @State in views.
@Observable
class SalesModel {
var monthlySales: [MonthlySale] = []
func load() async {
monthlySales = await SalesService.fetchMonthlySales()
}
}
struct MonthlySale: Identifiable {
let id = UUID()
let month: Date
let revenue: Double
let category: String
}struct SalesDashboard: View {
@State private var model = SalesModel()
var body: some View {
Chart(model.monthlySales) { item in
BarMark(
x: .value("Month", item.month, unit: .month),
y: .value("Revenue", item.revenue)
)
.foregroundStyle(by: .value("Category", item.category))
}
.task { await model.load() }
}
}Chart(data) { item in
BarMark(
x: .value("Department", item.department),
y: .value("Revenue", item.revenue)
)
}When multiple bars share the same x value, they stack automatically:
Chart(data) { item in
BarMark(
x: .value("Quarter", item.quarter),
y: .value("Sales", item.sales)
)
.foregroundStyle(by: .value("Product", item.product))
}Use .position(by:) to place bars side by side instead of stacking:
Chart(data) { item in
BarMark(
x: .value("Quarter", item.quarter),
y: .value("Sales", item.sales)
)
.foregroundStyle(by: .value("Product", item.product))
.position(by: .value("Product", item.product))
}Swap the x and y axes:
Chart(data) { item in
BarMark(
x: .value("Sales", item.sales),
y: .value("Region", item.region)
)
}
.chartYAxis {
AxisMarks { _ in
AxisValueLabel()
}
}Chart(data) { item in
BarMark(
x: .value("Quarter", item.quarter),
y: .value("Sales", item.sales),
stacking: .normalized
)
.foregroundStyle(by: .value("Product", item.product))
}Chart(data) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Revenue", item.revenue)
)
.annotation(position: .top, alignment: .center, spacing: 4) {
Text(item.revenue, format: .currency(code: "USD").precision(.fractionLength(0)))
.font(.caption2)
}
}Chart(tasks) { task in
BarMark(
xStart: .value("Start", task.startDate),
xEnd: .value("End", task.endDate),
y: .value("Task", task.name)
)
.foregroundStyle(by: .value("Status", task.status))
}Chart(data) { item in
LineMark(
x: .value("Date", item.date),
y: .value("Price", item.price)
)
PointMark(
x: .value("Date", item.date),
y: .value("Price", item.price)
)
.symbolSize(30)
}Chart(temperatures) { item in
LineMark(
x: .value("Date", item.date),
y: .value("Temp", item.temperature)
)
.foregroundStyle(by: .value("City", item.city))
.symbol(by: .value("City", item.city))
}Chart(data) { item in
AreaMark(
x: .value("Date", item.date),
y: .value("Value", item.value)
)
.foregroundStyle(
.linearGradient(
colors: [.blue.opacity(0.3), .blue.opacity(0.05)],
startPoint: .top,
endPoint: .bottom
)
)
LineMark(
x: .value("Date", item.date),
y: .value("Value", item.value)
)
.foregroundStyle(.blue)
}| Method | Use Case |
|---|---|
.linear | Default; straight segments between points |
.monotone | Smooth curve that preserves monotonicity |
.catmullRom | Smooth general-purpose curve |
.cardinal | Smooth with adjustable tension |
.stepStart | Step function starting at data point |
.stepCenter | Step function centered on data point |
.stepEnd | Step function ending at data point |
LineMark(x: .value("X", item.x), y: .value("Y", item.y))
.interpolationMethod(.monotone)Chart(recentData) { item in
LineMark(
x: .value("Time", item.time),
y: .value("Value", item.value)
)
.interpolationMethod(.catmullRom)
}
.chartXAxis(.hidden)
.chartYAxis(.hidden)
.chartLegend(.hidden)
.frame(width: 80, height: 30)Chart(products, id: \.name) { item in
SectorMark(angle: .value("Sales", item.sales))
.foregroundStyle(by: .value("Product", item.name))
}Chart(products, id: \.name) { item in
SectorMark(
angle: .value("Sales", item.sales),
innerRadius: .ratio(0.618),
outerRadius: .inset(10),
angularInset: 1
)
.cornerRadius(4)
.foregroundStyle(by: .value("Product", item.name))
}Chart(products, id: \.name) { item in
SectorMark(
angle: .value("Sales", item.sales),
innerRadius: .ratio(0.618),
angularInset: 1
)
.cornerRadius(4)
.foregroundStyle(by: .value("Product", item.name))
}
.chartBackground { _ in
VStack {
Text("Total")
.font(.caption)
.foregroundStyle(.secondary)
Text("\(totalSales, format: .number)")
.font(.title2.bold())
}
}@State private var selectedProduct: String?
Chart(products, id: \.name) { item in
SectorMark(
angle: .value("Sales", item.sales),
innerRadius: .ratio(0.618),
angularInset: 1
)
.cornerRadius(4)
.foregroundStyle(by: .value("Product", item.name))
.opacity(selectedProduct == nil || selectedProduct == item.name ? 1.0 : 0.4)
}
.chartAngleSelection(value: $selectedProduct)Limit pie/donut charts to 5-7 sectors. Group the rest into "Other":
func groupSmallSlices(_ data: [CategorySales], topN: Int = 5) -> [CategorySales] {
let sorted = data.sorted { $0.sales > $1.sales }
let top = Array(sorted.prefix(topN))
let otherTotal = sorted.dropFirst(topN).reduce(0) { $0 + $1.sales }
guard otherTotal > 0 else { return top }
return top + [CategorySales(name: "Other", sales: otherTotal)]
}Chart(data) { item in
AreaMark(
x: .value("Date", item.date),
yStart: .value("Min", item.low),
yEnd: .value("Max", item.high)
)
.foregroundStyle(.blue.opacity(0.15))
LineMark(
x: .value("Date", item.date),
y: .value("Average", item.average)
)
.foregroundStyle(.blue)
.lineStyle(StrokeStyle(lineWidth: 2))
}Chart {
ForEach(data) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Revenue", item.revenue)
)
}
RuleMark(y: .value("Target", targetRevenue))
.foregroundStyle(.red)
.lineStyle(StrokeStyle(lineWidth: 1, dash: [5, 3]))
.annotation(position: .top, alignment: .leading) {
Text("Target: \(targetRevenue, format: .number)")
.font(.caption)
.foregroundStyle(.red)
}
}Chart {
ForEach(data) { item in
PointMark(
x: .value("Experience", item.yearsExperience),
y: .value("Salary", item.salary)
)
.opacity(0.6)
}
LinePlot(x: "Experience", y: "Salary", domain: 0...20) { x in
baseSalary + x * salaryPerYear // linear trend
}
.foregroundStyle(.red)
.lineStyle(StrokeStyle(lineWidth: 1.5, dash: [4, 2]))
}Show a tooltip at the selected position using chartOverlay:
@State private var selectedDate: Date?
var body: some View {
Chart(data) { item in
LineMark(
x: .value("Date", item.date),
y: .value("Value", item.value)
)
if let selectedDate,
let match = data.first(where: { Calendar.current.isDate($0.date, inSameDayAs: selectedDate) }) {
RuleMark(x: .value("Selected", match.date))
.foregroundStyle(.secondary)
PointMark(
x: .value("Date", match.date),
y: .value("Value", match.value)
)
.symbolSize(60)
.annotation(position: .top) {
Text("\(match.value, format: .number)")
.font(.caption)
.padding(4)
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 4))
}
}
}
.chartXSelection(value: $selectedDate)
}@State private var scrollPosition: Date?
var body: some View {
Chart(dailySteps) { item in
BarMark(
x: .value("Date", item.date, unit: .day),
y: .value("Steps", item.steps)
)
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: 3600 * 24 * 7) // 7 days
.chartScrollPosition(x: $scrollPosition)
.chartScrollTargetBehavior(
.valueAligned(matching: DateComponents(hour: 0), majorAlignment: .page)
)
.chartXAxis {
AxisMarks(values: .stride(by: .day)) { value in
AxisGridLine()
AxisValueLabel(format: .dateTime.weekday(.abbreviated))
}
}
}Chart {
LinePlot(x: "x", y: "y", domain: -2 * .pi ... 2 * .pi) { x in
sin(x)
}
.foregroundStyle(.blue)
}
.chartYScale(domain: -1.5...1.5)Chart {
LinePlot(x: "x", y: "y", t: "t", domain: 0 ... 2 * .pi) { t in
(x: cos(t), y: sin(t))
}
}
.chartXScale(domain: -1.5...1.5)
.chartYScale(domain: -1.5...1.5)Chart {
AreaPlot(x: "x", yStart: "min", yEnd: "max", domain: 0...10) { x in
(yStart: sin(x) - 0.5, yEnd: sin(x) + 0.5)
}
.foregroundStyle(.blue.opacity(0.2))
}Swift Charts provides automatic VoiceOver descriptions for chart elements. The
framework reads axis labels and values to visually impaired users without
additional code. Ensure .value("Label", ...) strings are descriptive.
Chart(data) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Sales", item.sales)
)
.accessibilityLabel("Sales for \(item.month)")
.accessibilityValue("\(item.sales) units sold")
}BarPlot(data, x: .value("Month", \.month), y: .value("Sales", \.sales))
.accessibilityLabel(\.accessibilityDescription)
.accessibilityValue(\.formattedSales)The system automatically generates audio representations of chart data for VoiceOver users. Use clear, consistent data labels to ensure audio graphs convey meaningful patterns.
.value("Label", ...) -- these become VoiceOver labels..accessibilityLabel and .accessibilityValue for context beyond raw numbers..accessibilityHidden(true) on data-bearing marks.Charts automatically adjust axis label sizes with Dynamic Type. Avoid fixed frame heights that clip labels at larger text sizes.
// WRONG -- clips at large text sizes
Chart(data) { ... }
.frame(height: 200)
// CORRECT -- adaptive height
Chart(data) { ... }
.frame(minHeight: 200)
.frame(maxHeight: 400)Test charts at the "Accessibility Extra Extra Extra Large" text size to verify axis labels, annotations, and legends remain readable.
.foregroundStyle(by:) with
.symbol(by:) or .lineStyle(by:) for distinguishability.LineMark(x: .value("Date", item.date), y: .value("Value", item.value))
.foregroundStyle(by: .value("Category", item.category))
.symbol(by: .value("Category", item.category))
.lineStyle(by: .value("Category", item.category))For datasets exceeding 1000 data points, use vectorized plot types instead of individual marks. Vectorized plots accept entire collections and render efficiently.
| Data Points | Recommended Approach |
|---|---|
| < 100 | Individual marks (BarMark, LineMark, etc.) |
| 100 - 1000 | Either approach; profile if performance matters |
| > 1000 | Vectorized plots (BarPlot, LinePlot, etc.) |
struct SensorReading: Identifiable {
let id: Int
let timestamp: Date
let temperature: Double
var color: Color { temperature > 30 ? .red : .blue }
var accessibilityDescription: Text {
Text("\(timestamp.formatted(.dateTime.hour().minute())): \(temperature, specifier: "%.1f") degrees")
}
}
Chart {
LinePlot(
readings,
x: .value("Time", \.timestamp),
y: .value("Temperature", \.temperature)
)
.foregroundStyle(.blue)
}Apply KeyPath-based modifiers before simple-value modifiers:
// WRONG
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.opacity(0.8) // value modifier
.foregroundStyle(\.color) // KeyPath -- compiler error
// CORRECT
BarPlot(data, x: .value("X", \.x), y: .value("Y", \.y))
.foregroundStyle(\.color) // KeyPath first
.opacity(0.8) // value modifier second| Plot Type | Mark Equivalent | Available From |
|---|---|---|
BarPlot | BarMark | iOS 18+ |
LinePlot | LineMark | iOS 18+ |
PointPlot | PointMark | iOS 18+ |
AreaPlot | AreaMark | iOS 18+ |
RulePlot | RuleMark | iOS 18+ |
RectanglePlot | RectangleMark | iOS 18+ |
SectorPlot | SectorMark | iOS 18+ |
Swift Charts inherits the current color scheme automatically. System colors
(.blue, .orange, .green) adapt to light and dark modes without extra code.
Use .chartForegroundStyleScale to define a consistent palette:
Chart(data) { item in
BarMark(
x: .value("Category", item.category),
y: .value("Value", item.value)
)
.foregroundStyle(by: .value("Category", item.category))
}
.chartForegroundStyleScale([
"Electronics": .blue,
"Clothing": .purple,
"Food": .orange,
"Books": .green,
"Other": .gray
])Chart(data) { ... }
.chartPlotStyle { plotArea in
plotArea
.background(.quaternary.opacity(0.3))
.border(.quaternary, width: 0.5)
}.chartXAxisStyle { axis in
axis.background(.blue.opacity(0.05))
}Always preview charts in both light and dark color schemes. In Xcode previews:
#Preview {
ChartView()
.preferredColorScheme(.dark)
}Verify:
Chart(heatMapData) { item in
RectangleMark(
x: .value("Hour", item.hour),
y: .value("Day", item.day)
)
.foregroundStyle(by: .value("Count", item.count))
}
.chartForegroundStyleScale(range: Gradient(colors: [.blue, .yellow, .red]))| Method | Behavior |
|---|---|
.standard | Default. Regions stack on top showing absolute values. |
.normalized | Scales to 0-100% proportional view. |
.center | Baseline centered (streamgraph). |
.unstacked | Overlapping; no stacking. |
AreaMark(
x: .value("Date", item.date),
y: .value("Revenue", item.revenue),
stacking: .normalized
)
.foregroundStyle(by: .value("Category", item.category))| Dimension | Description |
|---|---|
.automatic | Framework decides |
.fixed(CGFloat) | Exact pixel size |
.inset(CGFloat) | Inset from available space |
.ratio(CGFloat) | Proportion of available space (0...1) |
Use for width, height on BarMark and innerRadius, outerRadius on SectorMark.
circle, square, triangle, diamond, pentagon, plus, cross, asterisk
PointMark(x: .value("X", item.x), y: .value("Y", item.y))
.symbol(.diamond)
.symbolSize(80)PointMark(x: .value("X", item.x), y: .value("Y", item.y))
.symbol(by: .value("Category", item.category))PointMark(x: .value("X", item.x), y: .value("Y", item.y))
.symbol {
Image(systemName: "star.fill")
.font(.caption2)
}Use chartOverlay or chartBackground to access ChartProxy:
.chartOverlay { proxy in
GeometryReader { geometry in
Rectangle()
.fill(.clear)
.contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
let origin = geometry[proxy.plotAreaFrame].origin
let location = CGPoint(
x: value.location.x - origin.x,
y: value.location.y - origin.y
)
if let date: Date = proxy.value(atX: location.x) {
selectedDate = date
}
}
)
}
}| Method | Purpose |
|---|---|
position(forX:) | Data value to screen x-coordinate |
position(forY:) | Data value to screen y-coordinate |
value(atX:as:) | Screen x-coordinate to data value |
value(atY:as:) | Screen y-coordinate to data value |
plotAreaSize | Size of the plot area |
plotAreaFrame | Anchor for the plot area frame |
chartXAxis(_:) / chartXAxis(content:)chartYAxis(_:) / chartYAxis(content:)chartXAxisLabel(...) / chartYAxisLabel(...)chartXAxisStyle(content:) / chartYAxisStyle(content:)chartXScale(domain:range:type:) and variantschartYScale(domain:range:type:) and variantschartForegroundStyleScale(_:) -- custom color mappingchartLegend(_:) -- visibilitychartLegend(position:alignment:spacing:) -- positioningchartLegend(position:alignment:spacing:content:) -- custom contentchartXSelection(value:) / chartXSelection(range:)chartYSelection(value:) / chartYSelection(range:)chartAngleSelection(value:) -- for SectorMarkchartScrollableAxes(_:)chartXVisibleDomain(length:) / chartYVisibleDomain(length:)chartScrollPosition(initialX:) / chartScrollPosition(x:)chartScrollTargetBehavior(_:)chartOverlay(alignment:content:) -- with ChartProxychartBackground(alignment:content:) -- with ChartProxychartPlotStyle(content:) -- plot area stylingskills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references