BS
BleepingSwift
Published on
6 min read

> Getting Started with Swift Charts

Apple introduced Swift Charts at WWDC 2022, giving SwiftUI developers a declarative way to build data visualizations. The framework follows SwiftUI's compositional approach, letting you construct charts from simple building blocks called marks.

Your First Chart

Creating a chart starts with the Chart view and one or more marks. Here's a simple bar chart:

import SwiftUI
import Charts

struct MonthlySales: Identifiable {
    let id = UUID()
    let month: String
    let revenue: Double
}

struct SalesChartView: View {
    let data: [MonthlySales] = [
        MonthlySales(month: "Jan", revenue: 5000),
        MonthlySales(month: "Feb", revenue: 8000),
        MonthlySales(month: "Mar", revenue: 7500),
        MonthlySales(month: "Apr", revenue: 12000),
        MonthlySales(month: "May", revenue: 9500),
        MonthlySales(month: "Jun", revenue: 11000)
    ]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("Month", item.month),
                y: .value("Revenue", item.revenue)
            )
        }
        .frame(height: 300)
        .padding()
    }
}

The Chart view takes a collection and a closure that returns marks for each data point. BarMark creates rectangular bars, with x and y defining the axes. The .value() function labels each axis for accessibility and tooltips.

Types of Marks

Swift Charts provides several mark types for different visualizations:

struct MultipleMarksView: View {
    let data: [MonthlySales] = [
        MonthlySales(month: "Jan", revenue: 5000),
        MonthlySales(month: "Feb", revenue: 8000),
        MonthlySales(month: "Mar", revenue: 7500),
        MonthlySales(month: "Apr", revenue: 12000)
    ]

    var body: some View {
        VStack(spacing: 40) {
            Chart(data) { item in
                LineMark(
                    x: .value("Month", item.month),
                    y: .value("Revenue", item.revenue)
                )
            }
            .frame(height: 150)

            Chart(data) { item in
                PointMark(
                    x: .value("Month", item.month),
                    y: .value("Revenue", item.revenue)
                )
            }
            .frame(height: 150)

            Chart(data) { item in
                AreaMark(
                    x: .value("Month", item.month),
                    y: .value("Revenue", item.revenue)
                )
            }
            .frame(height: 150)
        }
        .padding()
    }
}

LineMark connects points with lines, PointMark shows individual dots, and AreaMark fills the area under the line. You can combine multiple mark types in the same chart for richer visualizations.

Combining Marks

Layering marks creates more informative charts. Here's a line chart with points highlighted:

struct CombinedMarksView: View {
    let data: [MonthlySales] = [
        MonthlySales(month: "Jan", revenue: 5000),
        MonthlySales(month: "Feb", revenue: 8000),
        MonthlySales(month: "Mar", revenue: 7500),
        MonthlySales(month: "Apr", revenue: 12000)
    ]

    var body: some View {
        Chart(data) { item in
            LineMark(
                x: .value("Month", item.month),
                y: .value("Revenue", item.revenue)
            )
            .foregroundStyle(.blue)

            PointMark(
                x: .value("Month", item.month),
                y: .value("Revenue", item.revenue)
            )
            .foregroundStyle(.blue)
            .symbolSize(100)
        }
        .frame(height: 300)
        .padding()
    }
}

Working with Multiple Series

Charts often need to display multiple data series. Use foregroundStyle(by:) to differentiate them:

struct SalesByCategory: Identifiable {
    let id = UUID()
    let month: String
    let category: String
    let revenue: Double
}

struct MultiSeriesChartView: View {
    let data: [SalesByCategory] = [
        SalesByCategory(month: "Jan", category: "Electronics", revenue: 5000),
        SalesByCategory(month: "Jan", category: "Clothing", revenue: 3000),
        SalesByCategory(month: "Feb", category: "Electronics", revenue: 8000),
        SalesByCategory(month: "Feb", category: "Clothing", revenue: 4500),
        SalesByCategory(month: "Mar", category: "Electronics", revenue: 7500),
        SalesByCategory(month: "Mar", category: "Clothing", revenue: 5200),
        SalesByCategory(month: "Apr", category: "Electronics", revenue: 12000),
        SalesByCategory(month: "Apr", category: "Clothing", revenue: 6800)
    ]

    var body: some View {
        Chart(data) { item in
            LineMark(
                x: .value("Month", item.month),
                y: .value("Revenue", item.revenue)
            )
            .foregroundStyle(by: .value("Category", item.category))
            .symbol(by: .value("Category", item.category))
        }
        .frame(height: 300)
        .padding()
    }
}

Swift Charts automatically generates a legend and assigns distinct colors to each category.

Customizing Appearance

You can style charts with familiar SwiftUI modifiers:

struct StyledChartView: View {
    let data: [MonthlySales] = [
        MonthlySales(month: "Jan", revenue: 5000),
        MonthlySales(month: "Feb", revenue: 8000),
        MonthlySales(month: "Mar", revenue: 7500),
        MonthlySales(month: "Apr", revenue: 12000)
    ]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("Month", item.month),
                y: .value("Revenue", item.revenue)
            )
            .foregroundStyle(
                .linearGradient(
                    colors: [.blue, .purple],
                    startPoint: .bottom,
                    endPoint: .top
                )
            )
            .cornerRadius(8)
        }
        .chartYAxis {
            AxisMarks(position: .leading)
        }
        .chartXAxis {
            AxisMarks { value in
                AxisValueLabel()
                    .font(.caption)
            }
        }
        .frame(height: 300)
        .padding()
    }
}

The chartYAxis and chartXAxis modifiers give you control over axis appearance, positioning, and labels.

Date-Based Charts

For time series data, use Date values on the x-axis:

struct DailyMetric: Identifiable {
    let id = UUID()
    let date: Date
    let value: Double
}

struct TimeSeriesChartView: View {
    let data: [DailyMetric]

    init() {
        let calendar = Calendar.current
        let today = Date()
        data = (0..<14).map { dayOffset in
            let date = calendar.date(byAdding: .day, value: -dayOffset, to: today)!
            let value = Double.random(in: 100...500)
            return DailyMetric(date: date, value: value)
        }.reversed()
    }

    var body: some View {
        Chart(data) { item in
            LineMark(
                x: .value("Date", item.date, unit: .day),
                y: .value("Value", item.value)
            )
            .interpolationMethod(.catmullRom)

            AreaMark(
                x: .value("Date", item.date, unit: .day),
                y: .value("Value", item.value)
            )
            .foregroundStyle(.blue.opacity(0.1))
            .interpolationMethod(.catmullRom)
        }
        .chartXAxis {
            AxisMarks(values: .stride(by: .day, count: 3)) { value in
                AxisGridLine()
                AxisValueLabel(format: .dateTime.month().day())
            }
        }
        .frame(height: 300)
        .padding()
    }
}

The unit: .day parameter tells the chart to treat dates as discrete days. The interpolationMethod smooths the line between points.

Adding Interactivity

Swift Charts supports selection through the chartOverlay or chartGesture modifiers:

struct InteractiveChartView: View {
    let data: [MonthlySales] = [
        MonthlySales(month: "Jan", revenue: 5000),
        MonthlySales(month: "Feb", revenue: 8000),
        MonthlySales(month: "Mar", revenue: 7500),
        MonthlySales(month: "Apr", revenue: 12000),
        MonthlySales(month: "May", revenue: 9500),
        MonthlySales(month: "Jun", revenue: 11000)
    ]

    @State private var selectedMonth: String?

    var body: some View {
        VStack {
            Chart(data) { item in
                BarMark(
                    x: .value("Month", item.month),
                    y: .value("Revenue", item.revenue)
                )
                .foregroundStyle(selectedMonth == item.month ? .blue : .gray)
            }
            .chartXSelection(value: $selectedMonth)
            .frame(height: 300)

            if let month = selectedMonth,
               let item = data.first(where: { $0.month == month }) {
                Text("\(month): $\(Int(item.revenue))")
                    .font(.headline)
                    .padding()
            }
        }
        .padding()
    }
}

The chartXSelection modifier binds a selection state to tap or drag gestures on the chart.

Rule Marks for Reference Lines

Use RuleMark to add reference lines like averages or thresholds:

struct ChartWithReferenceLineView: View {
    let data: [MonthlySales] = [
        MonthlySales(month: "Jan", revenue: 5000),
        MonthlySales(month: "Feb", revenue: 8000),
        MonthlySales(month: "Mar", revenue: 7500),
        MonthlySales(month: "Apr", revenue: 12000),
        MonthlySales(month: "May", revenue: 9500),
        MonthlySales(month: "Jun", revenue: 11000)
    ]

    var average: Double {
        data.map(\.revenue).reduce(0, +) / Double(data.count)
    }

    var body: some View {
        Chart {
            ForEach(data) { item in
                BarMark(
                    x: .value("Month", item.month),
                    y: .value("Revenue", item.revenue)
                )
                .foregroundStyle(.blue)
            }

            RuleMark(y: .value("Average", average))
                .foregroundStyle(.orange)
                .lineStyle(StrokeStyle(lineWidth: 2, dash: [5, 5]))
                .annotation(position: .top, alignment: .leading) {
                    Text("Avg: $\(Int(average))")
                        .font(.caption)
                        .foregroundStyle(.orange)
                }
        }
        .frame(height: 300)
        .padding()
    }
}

Accessibility

Swift Charts automatically provides VoiceOver support through the labels you pass to .value(). You can enhance this with the accessibilityLabel modifier:

struct AccessibleChartView: View {
    let data: [MonthlySales] = [
        MonthlySales(month: "January", revenue: 5000),
        MonthlySales(month: "February", revenue: 8000),
        MonthlySales(month: "March", revenue: 7500)
    ]

    var body: some View {
        Chart(data) { item in
            BarMark(
                x: .value("Month", item.month),
                y: .value("Revenue", item.revenue)
            )
            .accessibilityLabel("\(item.month)")
            .accessibilityValue("$\(Int(item.revenue)) in revenue")
        }
        .frame(height: 300)
        .padding()
    }
}

VoiceOver users can swipe through each bar and hear the month and revenue values.

Swift Charts handles most of the complexity of data visualization for you. Define your data, choose appropriate marks, and let the framework manage scales, legends, and layout. For more advanced customizations like custom scales, annotations, or animations, check Apple's Swift Charts documentation.

Sample Project

Want to see this code in action? Check out the complete sample project on GitHub:

View on GitHub

The repository includes a working Xcode project with all the examples from this article, plus unit tests you can run to verify the behavior.

subscribe.sh

// Stay Updated

Get notified when I publish new tutorials on Swift, SwiftUI, and iOS development. No spam, unsubscribe anytime.

>

By subscribing, you agree to our Privacy Policy.