How to implement a bar chart in SwiftUI
Import Charts, wrap your data in a Chart view, and use BarMark(x: .value(...), y: .value(...)) for each data point. That's the entire public API — no third-party libraries needed.
import Charts
import SwiftUI
struct SalesBar: View {
let data = [("Jan", 42), ("Feb", 67), ("Mar", 55), ("Apr", 80)]
var body: some View {
Chart(data, id: \.0) { month, value in
BarMark(
x: .value("Month", month),
y: .value("Sales", value)
)
}
.frame(height: 200)
.padding()
}
}
Full implementation
The example below builds a polished monthly-sales bar chart with a custom color scheme, rounded bar tops, an annotated selection highlight, and a proper accessibility label on each bar. The @State-driven selection uses the .chartXSelection modifier introduced in iOS 17 — no gesture recognizers needed.
import Charts
import SwiftUI
// MARK: - Model
struct MonthlySale: Identifiable {
let id = UUID()
let month: String
let revenue: Double
}
// MARK: - View
struct SalesBarChartView: View {
let sales: [MonthlySale]
@State private var selectedMonth: String?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Monthly Revenue")
.font(.title2.bold())
.padding(.horizontal)
Chart(sales) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Revenue", item.revenue)
)
// Dim bars that are not selected
.foregroundStyle(
selectedMonth == nil || selectedMonth == item.month
? Color.accentColor
: Color.accentColor.opacity(0.3)
)
.cornerRadius(6)
// Accessible label for each bar
.accessibilityLabel("\(item.month)")
.accessibilityValue("$\(Int(item.revenue))k revenue")
// Annotation on the selected bar
if selectedMonth == item.month {
RuleMark(x: .value("Month", item.month))
.foregroundStyle(Color.accentColor.opacity(0.2))
.lineStyle(StrokeStyle(lineWidth: 2, dash: [5]))
.annotation(position: .top, alignment: .center) {
Text("$\(Int(item.revenue))k")
.font(.caption.bold())
.padding(6)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 6))
}
}
}
// Tap/drag to select a bar
.chartXSelection(value: $selectedMonth)
// Style the axes
.chartXAxis {
AxisMarks(values: .automatic) { _ in
AxisValueLabel()
.font(.caption)
}
}
.chartYAxis {
AxisMarks(preset: .automatic, position: .leading) { value in
AxisGridLine()
AxisValueLabel {
if let v = value.as(Double.self) {
Text("$\(Int(v))k")
.font(.caption)
}
}
}
}
.frame(height: 280)
.padding(.horizontal)
}
}
}
// MARK: - Preview
#Preview {
SalesBarChartView(sales: [
MonthlySale(month: "Jan", revenue: 42),
MonthlySale(month: "Feb", revenue: 67),
MonthlySale(month: "Mar", revenue: 55),
MonthlySale(month: "Apr", revenue: 80),
MonthlySale(month: "May", revenue: 73),
MonthlySale(month: "Jun", revenue: 91),
])
.padding(.vertical)
}
How it works
-
Chart container + BarMark.
Chart(sales)iterates your data collection and passes each element into the closure.BarMark(x: .value("Month", item.month), y: .value("Revenue", item.revenue))declares one vertical bar per element — the string on the x-axis becomes a categorical scale automatically. -
Dynamic foreground style. The
.foregroundStyle(...)modifier comparesselectedMonthagainst the current item's month. Unselected bars render at 30 % opacity, giving immediate visual feedback without custom drawing code. -
chartXSelection modifier (iOS 17+).
.chartXSelection(value: $selectedMonth)binds the chart's touch-drag interaction to your@Statestring. The framework handles hit-testing and snapping to the nearest bar automatically. -
RuleMark annotation. When
selectedMonthmatches the current item, aRuleMarkis overlaid at the same x-position, and a.annotationcallout floats above it with the formatted revenue value — all declarative, no geometry readers. -
Accessibility labels. Each
BarMarkhas.accessibilityLabeland.accessibilityValueapplied, so VoiceOver reads "January, $42k revenue" rather than a raw number, satisfying WCAG 1.1.1 non-text-content guidance.
Variants
Grouped bar chart (multiple series)
Add a foregroundStyle(by:) modifier to split each x-category into side-by-side bars — one per series. The Chart legend is rendered automatically.
struct GroupedSale: Identifiable {
let id = UUID()
let month: String
let category: String
let revenue: Double
}
struct GroupedBarChart: View {
let data: [GroupedSale]
var body: some View {
Chart(data) { item in
BarMark(
x: .value("Month", item.month),
y: .value("Revenue", item.revenue)
)
.foregroundStyle(by: .value("Category", item.category))
.cornerRadius(4)
}
.chartLegend(position: .bottom, alignment: .center)
.frame(height: 260)
.padding()
}
}
#Preview {
GroupedBarChart(data: [
GroupedSale(month: "Jan", category: "iOS", revenue: 42),
GroupedSale(month: "Jan", category: "macOS", revenue: 18),
GroupedSale(month: "Feb", category: "iOS", revenue: 67),
GroupedSale(month: "Feb", category: "macOS", revenue: 29),
])
}
Horizontal bar chart
Swap the roles of x and y in BarMark — pass the categorical label to y and the numeric value to x. SwiftUI Charts automatically reorients the axis scales and grid lines. This pattern works well for ranked lists (top countries, top screens) where labels are long strings that would overlap on a vertical x-axis.
BarMark(
x: .value("Revenue", item.revenue), // numeric → horizontal
y: .value("Month", item.month) // categorical → vertical
)
.cornerRadius(4)
Common pitfalls
-
⚠️ iOS 16 availability. The
Chartsframework ships with iOS 16, but.chartXSelectionand several annotation positions require iOS 17+. Gate those modifiers with@available(iOS 17, *)if you still need to support iOS 16, or drop support entirely — iOS 17 adoption crossed 90 % in early 2025. -
⚠️ Large datasets cause layout jank. Rendering hundreds of
BarMarkitems inside aScrollViewcan stutter on older devices because Charts re-measures the full axis range on every frame. Pre-aggregate your data to ~50 buckets max, or use.chartScrollableAxes(.horizontal)(iOS 17+) and let the framework virtualize the visible window. -
⚠️ Missing accessibility grouping. Without explicit
.accessibilityLabel/.accessibilityValueon each mark, VoiceOver reads the raw Double with full decimal precision ("42.000000 revenue"), which is confusing. Always format values to whole numbers or currency strings before passing them as accessibility values.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement a bar chart in SwiftUI for iOS 17+. Use Charts and BarMark. Support tap-to-select with a callout annotation using chartXSelection. Make it accessible (VoiceOver labels with formatted currency values). Add a #Preview with realistic monthly-revenue sample data.
Drop this prompt into Soarias during the Build phase to generate and instantly preview the component in the live simulator — no boilerplate needed before your first compile.
Related
FAQ
Does this work on iOS 16?
The base Chart + BarMark API is available from iOS 16.0, but interactive features like .chartXSelection and certain annotation positions require iOS 17. If your deployment target is iOS 16, wrap those modifiers in an if #available(iOS 17, *) check or use a separate view. For new apps in 2026, targeting iOS 17+ is strongly recommended — Apple's own analytics show iOS 16 below 5 % global share.
How do I animate bars when the data changes?
Wrap your data mutation in a withAnimation(.easeInOut) block. The Charts framework automatically interpolates bar heights between old and new values, producing a smooth grow/shrink transition. For initial appearance, apply .chartPlotStyle { $0.frame(height: 260) } and drive your data update inside .onAppear — Charts will animate from zero on first render.
What's the UIKit equivalent?
UIKit has no native bar chart API. In UIKit you would typically use a third-party library such as Charts (DanielGindi/Charts), Core Graphics with hand-drawn UIBezierPath rectangles, or wrap a SwiftUI bar chart view inside a UIHostingController. The SwiftUI Charts framework is the official Apple solution and is the recommended path for all new projects on iOS 16+.
Last reviewed: 2026-05-11 by the Soarias team.