Before Swift Charts, a bar chart meant a GeometryReader, manual scaling math against the tallest data point, and a stack of Rectangle views you positioned and sized by hand.
#The old way
import SwiftUI
struct HandRolledBarChart: View {
let data: [(day: String, sales: Double)]
var body: some View {
GeometryReader { geometry in
let maxSales = data.map(\.sales).max() ?? 1
HStack(alignment: .bottom, spacing: 8) {
ForEach(data, id: \.day) { point in
Rectangle()
.fill(Color.blue)
.frame(height: geometry.size.height * point.sales / maxSales)
}
}
}
}
}
#The new way
import SwiftUI
import Charts
struct SalesPoint: Identifiable {
let id = UUID()
let day: String
let sales: Double
}
struct SalesChart: View {
let data: [SalesPoint]
var body: some View {
Chart(data) { point in
BarMark(
x: .value("Day", point.day),
y: .value("Sales", point.sales)
)
.foregroundStyle(.blue)
}
.chartYAxisLabel("Sales ($)")
}
}
#Why it matters
- Marks (
BarMark,LineMark,PointMark,AreaMark) compose the same way SwiftUI views do — no manual geometry math for bar heights or point positions - Axes, legends, and scales are inferred from the data types passed in, and fully overridable when the defaults are wrong
- Mixing mark types in one
Chart— aLineMarktrend line overBarMarkbars — is just adding another mark, not a separate rendering pass - Accessibility (VoiceOver chart descriptions) and Dynamic Type on labels come from the framework, not something built by hand
#Gotcha
Chart re-renders marks from the data array on every state change like any SwiftUI view, so a chart backed by a large, frequently-updating dataset — a live sensor feed, for example — needs the same identity and diffing discipline as a List: give each point a stable Identifiable id, and avoid rebuilding the array from scratch on every tick. If bars aren't animating smoothly across data changes, check whether the array's identity is stable across updates before assuming the chart itself is broken.