Lektion 3.31 · Stufe 3 – Fortgeschritten

Diagramme mit Swift Charts

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du Daten mit Swift Charts als Diagramme darstellen: Balken, Linien, Flächen, Punkte und Kreis- bzw. Ringdiagramme. Du kannst Reihen nach Kategorien einfärben, mit Datumswerten arbeiten, Achsen und Farben anpassen, Linien für Durchschnittswerte einzeichnen und Werte per Fingertipp auswählen.

Das Problem: Zahlen sagen wenig

Eine Tabelle mit den Ausgaben der letzten 30 Tage enthält alle Informationen. Aber wann hast du am meisten ausgegeben? Wird es mehr oder weniger? Wofür geht das meiste Geld drauf? Um das aus 30 Zahlen herauszulesen, musst du lange hinschauen.

Ein Diagramm zeigt dieselben Informationen auf einen Blick. Mit Swift Charts baust du Diagramme so, wie du Views baust: deklarativ, mit wenigen Zeilen, und automatisch passend zu Dark Mode, Dynamic Type und VoiceOver.

Das erste Balkendiagramm

Swift
import SwiftUI
import Charts

struct DailySteps: Identifiable {
    let id = UUID()
    let day: String
    let steps: Int
}

struct StepsChart: View {
    let data = [
        DailySteps(day: "Mo", steps: 6_400),
        DailySteps(day: "Di", steps: 8_200),
        DailySteps(day: "Mi", steps: 4_900),
        DailySteps(day: "Do", steps: 10_300),
        DailySteps(day: "Fr", steps: 7_600),
        DailySteps(day: "Sa", steps: 12_100),
        DailySteps(day: "So", steps: 3_800)
    ]

    var body: some View {
        Chart(data) { entry in
            BarMark(
                x: .value("Tag", entry.day),
                y: .value("Schritte", entry.steps)
            )
        }
        .frame(height: 240)
        .padding()
    }
}

Erwartetes Ergebnis

Ein Balkendiagramm mit sieben Balken von Montag bis Sonntag. Die y-Achse ist automatisch beschriftet, die Balken haben die Akzentfarbe, und der Samstag sticht als höchster Balken heraus.

Screenshot folgt: Balkendiagramm mit Schritten pro Wochentag

Was hier passiert:

  • import Charts bindet das Framework ein.
  • Chart(data) { entry in ... } funktioniert wie List oder ForEach: Für jedes Element beschreibst du, was gezeichnet wird.
  • BarMark ist eine Markierung, hier ein Balken.
  • .value("Tag", entry.day) verbindet eine Beschriftung mit einem Wert. Die Beschriftung verwendet VoiceOver, um das Diagramm vorzulesen.

Achsen, Skalierung, Gitterlinien und Abstände berechnet Swift Charts selbst. Das ist der große Vorteil: Du beschreibst was, nicht wie.

Die Markierungen

Der Typ der Markierung bestimmt die Art des Diagramms. Tausch BarMark aus, und du bekommst ein anderes Diagramm mit denselben Daten:

Swift
LineMark(x: .value("Tag", entry.day), y: .value("Schritte", entry.steps))   // Linie
PointMark(x: .value("Tag", entry.day), y: .value("Schritte", entry.steps))  // Punkte
AreaMark(x: .value("Tag", entry.day), y: .value("Schritte", entry.steps))   // Fläche

Markierungen lassen sich auch kombinieren. Eine Linie mit Punkten:

Swift
Chart(data) { entry in
    LineMark(x: .value("Tag", entry.day), y: .value("Schritte", entry.steps))
        .interpolationMethod(.catmullRom)
    PointMark(x: .value("Tag", entry.day), y: .value("Schritte", entry.steps))
}

.interpolationMethod(.catmullRom) zeichnet eine weiche Kurve statt gerader Verbindungen.

Waagrechte Balken

Tauschst du x und y, liegen die Balken waagrecht. Das ist besonders gut für lange Beschriftungen:

Swift
BarMark(
    x: .value("Betrag", entry.amount),
    y: .value("Kategorie", entry.category)
)

Einfärben nach Kategorie

Oft gehören Werte zu verschiedenen Gruppen. Mit .foregroundStyle(by:) färbt Swift Charts jede Gruppe automatisch anders ein und erzeugt eine Legende:

Swift
struct Expense: Identifiable {
    let id = UUID()
    let month: String
    let category: String
    let amount: Double
}

let expenses = [
    Expense(month: "Jan", category: "Essen", amount: 320),
    Expense(month: "Jan", category: "Verkehr", amount: 90),
    Expense(month: "Jan", category: "Freizeit", amount: 150),
    Expense(month: "Feb", category: "Essen", amount: 290),
    Expense(month: "Feb", category: "Verkehr", amount: 110),
    Expense(month: "Feb", category: "Freizeit", amount: 80),
    Expense(month: "Mär", category: "Essen", amount: 340),
    Expense(month: "Mär", category: "Verkehr", amount: 95),
    Expense(month: "Mär", category: "Freizeit", amount: 210)
]

Chart(expenses) { expense in
    BarMark(
        x: .value("Monat", expense.month),
        y: .value("Betrag", expense.amount)
    )
    .foregroundStyle(by: .value("Kategorie", expense.category))
}

Erwartetes Ergebnis

Drei gestapelte Balken für Januar bis März, jeder aufgeteilt in drei Farben für Essen, Verkehr und Freizeit. Darunter erscheint automatisch eine Legende.

Balken mit demselben x-Wert werden standardmäßig gestapelt. Sollen sie nebeneinander stehen:

Swift
.position(by: .value("Kategorie", expense.category))

Bei Linien erzeugt .foregroundStyle(by:) eine eigene Linie pro Gruppe, etwa für den Vergleich mehrerer Jahre.

Eigene Farben

Die automatischen Farben kannst du durch eigene ersetzen:

Swift
.chartForegroundStyleScale([
    "Essen": Color.orange,
    "Verkehr": Color.blue,
    "Freizeit": Color.purple
])

Für ein einzelnes, einfarbiges Diagramm reicht .foregroundStyle(.orange) an der Markierung, auch mit Verlauf: .foregroundStyle(.orange.gradient).

Kreis- und Ringdiagramme

Für Anteile an einem Ganzen gibt es SectorMark:

Swift
struct CategoryTotal: Identifiable {
    let id = UUID()
    let name: String
    let total: Double
}

let totals = [
    CategoryTotal(name: "Essen", total: 950),
    CategoryTotal(name: "Verkehr", total: 295),
    CategoryTotal(name: "Freizeit", total: 440)
]

Chart(totals) { item in
    SectorMark(
        angle: .value("Betrag", item.total),
        innerRadius: .ratio(0.6),
        angularInset: 2
    )
    .foregroundStyle(by: .value("Kategorie", item.name))
    .cornerRadius(4)
}
.frame(height: 240)
  • angle: bestimmt die Größe des Stücks. Die Anteile berechnet Swift Charts selbst.
  • innerRadius: .ratio(0.6) macht aus dem Kuchen einen Ring mit 60 % Loch. Ohne diese Angabe entsteht ein volles Kreisdiagramm.
  • angularInset: 2 lässt kleine Lücken zwischen den Stücken.

In der Mitte des Rings ist Platz für eine Summe:

Swift
.chartBackground { _ in
    VStack {
        Text("Gesamt").font(.caption).foregroundStyle(.secondary)
        Text(1685, format: .currency(code: "EUR")).font(.title3.bold())
    }
}

Wann ein Kreisdiagramm?

Kreis- und Ringdiagramme eignen sich für wenige Anteile, etwa drei bis fünf. Bei mehr Kategorien oder ähnlich großen Werten kann das Auge die Stücke kaum vergleichen. Dann sind waagrechte Balken die bessere Wahl.

Datumswerte

Für Zeitverläufe übergibst du echte Date-Werte und sagst, in welcher Einheit sie zusammengefasst werden sollen:

Swift
struct DayAmount: Identifiable {
    let id = UUID()
    let date: Date
    let amount: Double
}

Chart(days) { day in
    BarMark(
        x: .value("Tag", day.date, unit: .day),
        y: .value("Betrag", day.amount)
    )
}
.chartXAxis {
    AxisMarks(values: .stride(by: .day, count: 7)) { _ in
        AxisGridLine()
        AxisValueLabel(format: .dateTime.day().month(.abbreviated))
    }
}
  • unit: .day sorgt dafür, dass jeder Balken genau einen Tag breit ist. Mit .month oder .weekOfYear fasst Swift Charts Werte desselben Monats oder derselben Woche zusammen.
  • .chartXAxis passt die Achse an: Hier gibt es nur jede Woche eine Beschriftung, formatiert als „12. Okt.“.

Achsen und Skalen anpassen

Ein paar nützliche Einstellungen:

Swift
.chartYScale(domain: 0...15_000)          // feste Spannweite der y-Achse
.chartYAxis(.hidden)                      // y-Achse ausblenden
.chartXAxisLabel("Wochentag")             // Achsenbeschriftung
.chartLegend(.hidden)                     // Legende ausblenden
.chartLegend(position: .top)              // Legende oben

Eine feste Spannweite ist wichtig, wenn mehrere Diagramme vergleichbar sein sollen, oder damit sich die Achse beim Aktualisieren nicht ständig verändert.

Referenzlinien

Ein Durchschnitt oder ein Ziel lässt sich als Linie einzeichnen, mit RuleMark:

Swift
Chart {
    ForEach(data) { entry in
        BarMark(x: .value("Tag", entry.day), y: .value("Schritte", entry.steps))
            .foregroundStyle(entry.steps >= 10_000 ? Color.green : Color.orange)
    }

    RuleMark(y: .value("Ziel", 10_000))
        .lineStyle(StrokeStyle(lineWidth: 1, dash: [5]))
        .foregroundStyle(.secondary)
        .annotation(position: .top, alignment: .leading) {
            Text("Ziel").font(.caption).foregroundStyle(.secondary)
        }
}

Beachte die zweite Schreibweise: Chart { ... } ohne Daten, mit einem ForEach darin. So kannst du Markierungen aus verschiedenen Quellen in ein Diagramm packen: die Balken aus dem Array und dazu eine einzelne Linie.

  • RuleMark(y:) zeichnet eine waagrechte Linie über die ganze Breite.
  • .lineStyle(StrokeStyle(lineWidth: 1, dash: [5])) macht sie gestrichelt.
  • .annotation hängt eine beliebige View an eine Markierung, hier die Beschriftung „Ziel“.

Die Balken sind grün, wenn das Ziel erreicht wurde, sonst orange. Jede Markierung kann ihre eigene Farbe haben.

Werte auswählen

Tippt oder streicht der Nutzer über das Diagramm, soll der genaue Wert erscheinen. Dafür gibt es .chartXSelection:

Swift
struct InteractiveStepsChart: View {
    let data: [DailySteps]
    @State private var selectedDay: String?

    var body: some View {
        Chart(data) { entry in
            BarMark(x: .value("Tag", entry.day), y: .value("Schritte", entry.steps))
                .foregroundStyle(selectedDay == nil || selectedDay == entry.day ? Color.orange : Color.orange.opacity(0.3))

            if entry.day == selectedDay {
                RuleMark(x: .value("Tag", entry.day))
                    .foregroundStyle(.clear)
                    .annotation(position: .top) {
                        Text("\(entry.steps) Schritte")
                            .font(.caption.bold())
                            .padding(6)
                            .background(.regularMaterial, in: .rect(cornerRadius: 6))
                    }
            }
        }
        .chartXSelection(value: $selectedDay)
        .frame(height: 240)
    }
}
  • .chartXSelection(value: $selectedDay) schreibt den x-Wert unter dem Finger in selectedDay, solange der Finger auf dem Diagramm liegt. Danach wird er wieder nil.
  • Der gewählte Balken bleibt kräftig, die anderen werden blasser.
  • Eine unsichtbare RuleMark beim gewählten Tag trägt die Beschriftung mit dem genauen Wert.

Tipp

Bei Datumsachsen ist die Auswahl ein Date?. Um den passenden Eintrag zu finden, vergleichst du mit Calendar.current.isDate(entry.date, inSameDayAs: selected).

Animation

Ändern sich die Daten mit withAnimation, wachsen und schrumpfen die Balken automatisch:

Swift
Button("Neue Woche") {
    withAnimation(.smooth) {
        data = generateRandomWeek()
    }
}

Weil jedes Element eine stabile id hat, weiß Swift Charts, welcher Balken zu welchem wird.

Typische Fehler

Cannot find 'Chart' in scope

import Charts fehlt oben in der Datei.

Balken in falscher Reihenfolge

Bei Text-Werten auf der x-Achse ordnet Swift Charts nach dem ersten Auftreten. Sortiere die Daten vorher, oder lege die Reihenfolge fest mit .chartXScale(domain: ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]).

Datumsbalken sind hauchdünn

Ohne unit: behandelt Swift Charts Datumswerte als exakte Zeitpunkte, und Balken bekommen kaum Breite. Gib unit: .day oder eine andere passende Einheit an.

Diagramm füllt den ganzen Bildschirm

Ein Chart nimmt sich so viel Platz, wie er bekommt. Gib ihm eine Höhe mit .frame(height:).

FAQ

Anfänger

Was bedeutet das überhaupt?

Muss ich Swift Charts extra installieren? Nein, es ist Teil von iOS und muss nur importiert werden.

Kann VoiceOver Diagramme vorlesen? Ja, automatisch. Dafür sind die Beschriftungen in .value("Tag", ...) wichtig: VoiceOver liest etwa „Tag: Mo, Schritte: 6400“. Mit „Audiografik“ kann es den Verlauf sogar als Tonfolge abspielen.

Fortgeschritten

Warum verhält es sich so?

Wie mache ich ein Diagramm waagrecht scrollbar, etwa für ein ganzes Jahr? Mit .chartScrollableAxes(.horizontal) und .chartXVisibleDomain(length: ...), das festlegt, wie viel auf einmal sichtbar ist. Bei Datumswerten ist die Länge in Sekunden, etwa 3600 * 24 * 30 für 30 Tage.

Wie zeichne ich eine Funktion statt Datenpunkten? Mit LinePlot(x: "x", y: "y") { x in sin(x) } und .chartXScale(domain: -10...10). Swift Charts berechnet die Punkte selbst. Das ist praktisch für Lern- und Mathe-Apps.

Profi

Was passiert unter der Haube?

Wie viele Datenpunkte schafft Swift Charts? Mit einzelnen Markierungen einige tausend flüssig. Für sehr große Datenmengen gibt es Vectorized Plots wie BarPlot(data, x: .value(...), y: .value(...)), die ganze Sammlungen auf einmal zeichnen und deutlich schneller sind. Bei Zehntausenden Punkten lohnt es sich außerdem, die Daten vorher zusammenzufassen.

Wie greife ich auf Positionen im Diagramm zu, etwa für eigene Gesten? Über .chartOverlay { proxy in ... }. Der ChartProxy rechnet zwischen Bildschirmpositionen und Datenwerten um: proxy.value(atX: location.x, as: String.self) liefert den Wert unter einer Fingerposition. Damit lassen sich eigene Auswahl- und Zoom-Gesten bauen.

Übungen

Anfänger

Übung 1: Temperaturen

Zeig die Temperaturen einer Woche als Liniendiagramm mit Punkten und weicher Kurve. Färbe die Linie mit einem Verlauf von Blau nach Rot.

Fortgeschritten

Übung 2: Schritte mit Ziel

Baue das Schritte-Diagramm mit Ziellinie und färbe Balken über dem Ziel grün. Füge .chartXSelection hinzu, sodass beim Darüberstreichen der genaue Wert erscheint. Darunter steht der Durchschnitt der Woche.

Profi

Übung 3: Monatsübersicht

Erzeuge zufällige Ausgaben für die letzten 30 Tage in drei Kategorien. Zeig darüber ein Ringdiagramm der Anteile mit der Summe in der Mitte und darunter ein Balkendiagramm pro Tag, gestapelt nach Kategorie, mit Beschriftung nur jede Woche. Beide Diagramme verwenden dieselben Farben.

Lösung Übung 3Erst selbst probieren!
Swift
import SwiftUI
import Charts

struct Spending: Identifiable {
    let id = UUID()
    let date: Date
    let category: String
    let amount: Double
}

struct MonthOverview: View {
    let categories = ["Essen", "Verkehr", "Freizeit"]
    let colors: [String: Color] = ["Essen": .orange, "Verkehr": .blue, "Freizeit": .purple]

    let spendings: [Spending] = {
        let today = Calendar.current.startOfDay(for: .now)
        return (0..<30).flatMap { offset in
            ["Essen", "Verkehr", "Freizeit"].map { category in
                Spending(
                    date: Calendar.current.date(byAdding: .day, value: -offset, to: today)!,
                    category: category,
                    amount: Double.random(in: 0...40)
                )
            }
        }
    }()

    var total: Double { spendings.reduce(0) { $0 + $1.amount } }

    func total(for category: String) -> Double {
        spendings.filter { $0.category == category }.reduce(0) { $0 + $1.amount }
    }

    var body: some View {
        ScrollView {
            VStack(spacing: 32) {
                Chart(categories, id: \.self) { category in
                    SectorMark(angle: .value("Betrag", total(for: category)),
                               innerRadius: .ratio(0.6), angularInset: 2)
                        .foregroundStyle(by: .value("Kategorie", category))
                }
                .chartBackground { _ in
                    Text(total, format: .currency(code: "EUR")).font(.title3.bold())
                }
                .chartForegroundStyleScale(domain: categories, range: categories.map { colors[$0]! })
                .frame(height: 220)

                Chart(spendings) { spending in
                    BarMark(x: .value("Tag", spending.date, unit: .day),
                            y: .value("Betrag", spending.amount))
                        .foregroundStyle(by: .value("Kategorie", spending.category))
                }
                .chartXAxis {
                    AxisMarks(values: .stride(by: .day, count: 7)) { _ in
                        AxisGridLine()
                        AxisValueLabel(format: .dateTime.day().month(.abbreviated))
                    }
                }
                .chartForegroundStyleScale(domain: categories, range: categories.map { colors[$0]! })
                .frame(height: 220)
            }
            .padding()
        }
    }
}

Beide Diagramme bekommen dieselbe .chartForegroundStyleScale mit derselben Reihenfolge der Kategorien. So hat jede Kategorie überall dieselbe Farbe.

Zusammenfassung

  • Swift Charts baut Diagramme deklarativ: Chart(data) { BarMark(...) }. Vorher import Charts.
  • Markierungen: BarMark, LineMark, PointMark, AreaMark, SectorMark, RuleMark. Sie lassen sich kombinieren.
  • .value("Beschriftung", wert) verbindet Werte mit Namen, auch für VoiceOver.
  • .foregroundStyle(by:) färbt nach Kategorie und erzeugt eine Legende, .chartForegroundStyleScale legt die Farben fest.
  • Datumswerte mit unit:, Achsen mit .chartXAxis und .chartYScale anpassen.
  • RuleMark und .annotation für Ziel- und Durchschnittslinien.
  • .chartXSelection(value:) für interaktive Auswahl.

Spickzettel

Swift
import Charts

Chart(data) { item in
    BarMark(x: .value("Tag", item.date, unit: .day),
            y: .value("Betrag", item.amount))
        .foregroundStyle(by: .value("Kategorie", item.category))
}
.chartForegroundStyleScale(["Essen": Color.orange, "Verkehr": Color.blue])
.chartYScale(domain: 0...500)
.chartXSelection(value: $selected)
.frame(height: 240)

SectorMark(angle: .value("Betrag", x), innerRadius: .ratio(0.6), angularInset: 2)

RuleMark(y: .value("Ziel", 100))
    .lineStyle(StrokeStyle(lineWidth: 1, dash: [5]))
    .annotation(position: .top) { Text("Ziel") }

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.