Lektion 3.30 · Stufe 3 – Fortgeschritten

Dark Mode, Dynamic Type und aktuelles Apple-Design

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du Apps bauen, die in jeder Einstellung gut aussehen: im hellen und dunklen Modus, mit kleiner und sehr großer Schrift und im aktuellen Apple-Design mit Liquid Glass. Du kennst @ScaledMetric, weißt, wie du Layouts bei großer Schrift umstellst, und setzt Materialien und Glas-Effekte gezielt ein.

Das Problem: Deine App sieht nur bei dir gut aus

Du entwickelst deine App auf deinem iPhone, mit deinen Einstellungen. Vielleicht im hellen Modus, mit normaler Schriftgröße. Dort sieht alles perfekt aus.

Deine Nutzer haben aber andere Einstellungen:

  • Fast die Hälfte nutzt den Dark Mode, viele davon nur abends automatisch.
  • Rund jeder Vierte hat eine größere Schrift eingestellt, manche eine sehr große.
  • Manche haben erhöhten Kontrast oder reduzierte Transparenz aktiviert.

Eine App, die nur mit deinen Einstellungen funktioniert, wirkt für viele Menschen kaputt: weiße Flecken im Dunkeln, abgeschnittene Texte, unlesbare Farben. Die gute Nachricht: Wenn du ein paar Regeln beachtest, macht SwiftUI das meiste automatisch richtig.

Dark Mode

Was automatisch funktioniert

Alle semantischen Farben passen sich von selbst an (Lektion 2.6 und 3.29):

Swift
Text("Titel").foregroundStyle(.primary)              // schwarz bzw. weiß
Text("Info").foregroundStyle(.secondary)             // grau, passend
.background(Color(.systemBackground))                // weiß bzw. schwarz
.background(Color(.secondarySystemBackground))       // hellgrau bzw. dunkelgrau

Ebenso alle Standard-Komponenten wie List, Form, Button, Toggle und alle Farben aus dem Asset-Katalog mit Dunkel-Variante.

Testen in der Vorschau

Unten im Canvas über die Variants-Schaltfläche kannst du „Color Scheme Variants“ wählen und siehst die View gleichzeitig hell und dunkel. Oder du fügst einer Preview .preferredColorScheme(.dark) hinzu. Im Simulator schaltest du mit ⇧ ⌘ A um.

Den Modus abfragen

Manchmal musst du im Code wissen, welcher Modus aktiv ist, etwa um einen Schatten im Dunkeln wegzulassen (Lektion 3.28):

Swift
@Environment(\.colorScheme) private var colorScheme

var body: some View {
    Image("Logo")
        .opacity(colorScheme == .dark ? 0.85 : 1)
}

colorScheme ist .light oder .dark. Verwende es sparsam: Meist ist eine Farbe mit Dunkel-Variante im Asset-Katalog die bessere Lösung.

Bilder im Dark Mode

Auch Bilder im Asset-Katalog können eine Dunkel-Variante haben: Bild auswählen, im Attributes Inspector bei Appearances „Any, Dark“ einstellen und ein zweites Bild hineinziehen. SF Symbols passen sich ohnehin automatisch an.

Einen Modus erzwingen?

Swift
.preferredColorScheme(.dark)

Damit kannst du die ganze App auf einen Modus festlegen. Tu das nur aus gutem Grund, etwa für eine Kamera- oder Film-App, die immer dunkel sein soll. Nutzer erwarten, dass ihre Einstellung respektiert wird. Gut ist es dagegen, in den Einstellungen deiner App eine Wahl „System, Hell, Dunkel“ anzubieten und mit @AppStorage zu speichern (Lektion 3.13).

Dynamic Type

In Lektion 2.5 hast du gelernt: Textstile wie .body und .headline wachsen automatisch mit der eingestellten Schriftgröße. Das ist die halbe Miete. Die andere Hälfte: Auch das Layout muss mitwachsen.

Testen

Stell in der Vorschau über die Variants-Schaltfläche „Dynamic Type Variants“ ein, und du siehst deine View in allen Größen von „xSmall“ bis „AX5“. Oder füge .dynamicTypeSize(.accessibility3) an die Preview an.

Die Größen ab .accessibility1 heißen Bedienungshilfe-Größen. Hier wird Text wirklich groß, und Layouts mit vielen Elementen nebeneinander geraten an ihre Grenzen.

Abstände und Symbole mitwachsen lassen: @ScaledMetric

Text wächst, aber feste Zahlen wie Abstände oder Bildgrößen nicht. Ein 24-Punkte-Symbol neben riesigem Text wirkt winzig. Dafür gibt es @ScaledMetric:

Swift
struct RecipeRow: View {
    @ScaledMetric private var imageSize = 56.0
    @ScaledMetric(relativeTo: .caption) private var spacing = 4.0

    var body: some View {
        HStack {
            RoundedRectangle(cornerRadius: 8)
                .fill(.orange.gradient)
                .frame(width: imageSize, height: imageSize)
            VStack(alignment: .leading, spacing: spacing) {
                Text("Pfannkuchen").font(.headline)
                Text("30 Minuten").font(.caption)
            }
        }
    }
}
  • @ScaledMetric nimmt einen Wert für die Standardgröße und skaliert ihn im selben Verhältnis wie die Schrift.
  • relativeTo: gibt an, mit welchem Textstil er wachsen soll. Standard ist .body.

Bei großer Schrift wird das Bild entsprechend größer und der Abstand passt sich an. Die Proportionen bleiben erhalten.

Layout bei großer Schrift umstellen

Bei sehr großer Schrift passen Elemente oft nicht mehr nebeneinander. Die Lösung: untereinander anordnen. Dafür liest du die Schriftgröße aus der Environment:

Swift
struct InfoRow: View {
    @Environment(\.dynamicTypeSize) private var typeSize

    var body: some View {
        let layout = typeSize.isAccessibilitySize
            ? AnyLayout(VStackLayout(alignment: .leading, spacing: 4))
            : AnyLayout(HStackLayout())

        layout {
            Label("Zubereitung", systemImage: "clock")
            Spacer(minLength: 0)
            Text("30 Minuten").foregroundStyle(.secondary)
        }
    }
}
  • typeSize.isAccessibilitySize ist true ab den Bedienungshilfe-Größen.
  • AnyLayout verpackt ein Layout so, dass du zwischen HStackLayout und VStackLayout wechseln kannst, ohne dass die Kinder neu erzeugt werden. Ihr Zustand bleibt erhalten, und der Wechsel lässt sich sogar animieren.

Alternativ funktioniert auch ViewThatFits aus Lektion 3.19, das automatisch auf Platzmangel reagiert.

Größe begrenzen, wo es nötig ist

Manche Elemente haben einfach keinen Platz zum Wachsen, etwa ein kleines Badge in einer Toolbar. Dort kannst du den Bereich der Schriftgrößen begrenzen:

Swift
Text("Neu")
    .font(.caption.bold())
    .dynamicTypeSize(...DynamicTypeSize.xxxLarge)

...DynamicTypeSize.xxxLarge ist ein Bereich (Lektion 1.5): bis höchstens xxxLarge. Größer wird es nicht.

Achtung

Begrenze nur, wo es wirklich nicht anders geht, und nie für längere Texte. Wer große Schrift einstellt, braucht sie.

Lange Texte nicht abschneiden

Swift
Text(recipe.name)
    .lineLimit(1)   // ⚠️ bei großer Schrift abgeschnitten

Bei großer Schrift wird aus „Spaghetti Carbonara“ schnell „Spaghetti Ca…“. Lass Texte umbrechen oder erlaube bei Bedarf mehr Zeilen: .lineLimit(1...3) erlaubt eine bis drei Zeilen.

Materialien

Materialien sind halbdurchsichtige, unscharfe Hintergründe, wie Milchglas. Sie lassen den Inhalt dahinter erahnen und heben das Element trotzdem ab:

Swift
Text("Gespeichert")
    .padding()
    .background(.regularMaterial, in: .rect(cornerRadius: 12))

Es gibt sie in mehreren Stärken: .ultraThinMaterial, .thinMaterial, .regularMaterial, .thickMaterial und .ultraThickMaterial. Je dicker, desto weniger scheint durch.

Materialien passen sich automatisch an Hell und Dunkel an und bleiben auf jedem Hintergrund lesbar. Text darauf mit .primary und .secondary bekommt automatisch einen passenden, leicht „eingefärbten“ Ton.

Liquid Glass: das aktuelle Apple-Design

Seit iOS 26 verwendet Apple ein neues Design namens Liquid Glass: Navigationsleisten, Tab-Leisten, Toolbars, Buttons und Schalter bestehen aus einem dynamischen, glasartigen Material, das Licht bricht, auf Berührung reagiert und sich an den Inhalt darunter anpasst.

Was du geschenkt bekommst

Das Wichtigste zuerst: Wenn du Standard-Komponenten verwendest, bekommst du Liquid Glass automatisch. NavigationStack, TabView, Toolbars, Sheets, Menüs, Toggle, Slider und Buttons in Toolbars nehmen das neue Aussehen an, sobald du deine App mit dem aktuellen Xcode baust. Du musst nichts ändern.

Das ist einer der größten Vorteile davon, bei Apples Bausteinen zu bleiben, statt alles selbst zu gestalten.

Glas-Buttons

Für eigene Buttons gibt es passende Stile:

Swift
Button("Abspielen", systemImage: "play.fill") { }
    .buttonStyle(.glass)

Button("Bestellen") { }
    .buttonStyle(.glassProminent)
  • .glass: ein durchsichtiger Glas-Button, gut für sekundäre Aktionen über Bildern oder Karten.
  • .glassProminent: ein eingefärbter Glas-Button in der Akzentfarbe für die Hauptaktion.

Eigene Elemente aus Glas

Mit .glassEffect bekommt jede View einen Glas-Hintergrund:

Swift
Label("12 Min.", systemImage: "timer")
    .padding(.horizontal, 14)
    .padding(.vertical, 8)
    .glassEffect()

Standardmäßig ist die Form eine Kapsel. Du kannst sie ändern, das Glas einfärben und es auf Berührung reagieren lassen:

Swift
.glassEffect(.regular.tint(.orange).interactive(), in: .rect(cornerRadius: 16))
  • .tint(.orange) färbt das Glas leicht ein.
  • .interactive() lässt es beim Antippen reagieren: Es leuchtet auf und wölbt sich leicht.

Mehrere Glas-Elemente zusammen

Liegen mehrere Glas-Elemente nah beieinander, sollten sie in einem GlassEffectContainer stehen:

Swift
GlassEffectContainer(spacing: 20) {
    HStack(spacing: 20) {
        Image(systemName: "pencil")
            .frame(width: 44, height: 44)
            .glassEffect()
        Image(systemName: "eraser")
            .frame(width: 44, height: 44)
            .glassEffect()
    }
}

Glas kann kein Glas spiegeln. Der Container sorgt dafür, dass die Elemente sich ein gemeinsames Glas teilen und sogar ineinander verschmelzen, wenn sie sich nahe kommen. Mit .glassEffectID(_:in:) und einem @Namespace (Lektion 3.25) lassen sich Glas-Elemente animiert teilen und zusammenführen.

Glas für Bedienelemente, nicht für Inhalte

Liquid Glass ist für die Bedienebene gedacht: Leisten, schwebende Buttons, Steuerelemente, die über dem Inhalt liegen. Der Inhalt selbst, also Listen, Karten, Texte, Bilder, bekommt kein Glas. Zu viel Glas macht eine App unruhig und schwer lesbar. Glas auf Glas ist ebenfalls zu vermeiden.

Barrierefreiheit kommt mit

Hat ein Nutzer „Transparenz reduzieren“ oder „Kontrast erhöhen“ eingeschaltet, passt das System Liquid Glass und Materialien automatisch an: Sie werden undurchsichtiger und kontrastreicher. Auch das bekommst du nur, wenn du Apples Materialien und Effekte verwendest statt eigener Nachbauten mit opacity und blur.

Typische Fehler

Weiße Flecken im Dark Mode

.background(.white) oder Color(red: 1, green: 1, blue: 1). Verwende Color(.systemBackground) oder Asset-Farben mit Dunkel-Variante.

Text auf Bildern unlesbar

Weißer Text auf einem hellen Foto verschwindet. Leg einen Verlauf oder ein Material darunter: .background(.ultraThinMaterial) oder ein LinearGradient von Schwarz mit geringer Deckkraft nach Durchsichtig.

Abgeschnittene Texte bei großer Schrift

.lineLimit(1) oder feste Höhen mit .frame(height:) um Text. Lass Texte umbrechen und vermeide feste Höhen für alles, was Text enthält.

Eigenes „Glas“ mit Blur

Swift
.background(.white.opacity(0.3)).blur(radius: 10)   // ⚠️

Selbstgebaute Effekte passen sich weder an Hell und Dunkel noch an Bedienungshilfen an. Nimm Materialien oder .glassEffect.

FAQ

Anfänger

Was bedeutet das überhaupt?

Muss ich Dark Mode unterstützen? Ja. Apple erwartet es, Nutzer erwarten es, und mit Systemfarben kostet es fast keine Arbeit. Ohne Anpassung wirkt deine App im Dark Mode wie ein greller Fremdkörper.

Was bedeutet „AX5“? Die größte Bedienungshilfe-Schriftgröße, .accessibility5. Text ist dort etwa dreimal so groß wie normal. Wenn deine App auch dort benutzbar bleibt, ist sie für alle gut benutzbar.

Fortgeschritten

Warum verhält es sich so?

Wie teste ich alles auf einmal? Mit dem Environment Overrides-Button in der Debug-Leiste von Xcode, während die App im Simulator läuft. Dort kannst du Dark Mode, Schriftgröße, erhöhten Kontrast und reduzierte Bewegung live umschalten, ohne die Einstellungen zu öffnen.

Was ist mit älteren iOS-Versionen und Liquid Glass? .glassEffect und die Glas-Button-Stile gibt es erst ab iOS 26. Unterstützt deine App ältere Versionen, prüfe mit if #available(iOS 26, *) und verwende sonst ein Material. Standard-Komponenten zeigen auf jeder Version automatisch das passende Design.

Profi

Was passiert unter der Haube?

Wie funktioniert @ScaledMetric intern? Es nutzt dieselbe Skalierungskurve wie der angegebene Textstil. Diese Kurve ist nicht linear: Kleine Stile wachsen bei großen Einstellungen stärker als große Stile. Deshalb lohnt es sich, relativeTo: passend zum Text neben dem Element zu wählen.

Warum soll man Glas in einem Container gruppieren? Liquid Glass sampelt den Inhalt darunter, um Lichtbrechung und Farbe zu berechnen. Mehrere unabhängige Glas-Elemente würden sich gegenseitig samplen, was teuer ist und inkonsistent aussieht. Der Container rendert alle Elemente in einem gemeinsamen Durchgang und macht so auch das Verschmelzen möglich.

Übungen

Anfänger

Übung 1: Alles im Dunkeln

Nimm eine deiner bisherigen Apps, etwa die aus Projekt C, und sieh sie dir in der Vorschau mit „Color Scheme Variants“ an. Ersetze alle festen Farben, die im Dark Mode stören, durch Systemfarben oder Asset-Farben.

Fortgeschritten

Übung 2: Mitwachsende Zeile

Baue eine Zeile aus Symbol, Titel und Wert. Das Symbol wächst mit @ScaledMetric mit. Ab den Bedienungshilfe-Größen stehen Titel und Wert untereinander statt nebeneinander. Prüfe das Ergebnis mit „Dynamic Type Variants“.

Lösung Übung 2Erst selbst probieren!
Swift
struct StatRow: View {
    let icon: String
    let title: String
    let value: String

    @ScaledMetric private var iconSize = 28.0
    @Environment(\.dynamicTypeSize) private var typeSize

    var body: some View {
        HStack(alignment: .top, spacing: 12) {
            Image(systemName: icon)
                .font(.system(size: iconSize * 0.6))
                .frame(width: iconSize, height: iconSize)
                .foregroundStyle(.white)
                .background(.orange, in: .rect(cornerRadius: 6))

            let layout = typeSize.isAccessibilitySize
                ? AnyLayout(VStackLayout(alignment: .leading))
                : AnyLayout(HStackLayout())

            layout {
                Text(title)
                Spacer(minLength: 0)
                Text(value).foregroundStyle(.secondary)
            }
        }
    }
}

Die Symbolgröße wird aus iconSize berechnet, damit auch das Symbol selbst mitwächst.

Profi

Übung 3: Schwebende Glas-Leiste

Baue über einem bildschirmfüllenden Farbverlauf eine schwebende Leiste am unteren Rand mit drei Glas-Buttons in einem GlassEffectContainer. Ein vierter Button „Mehr“ soll beim Antippen zwei zusätzliche Buttons aus sich herauswachsen lassen. Verwende .glassEffectID und einen @Namespace.

Zusammenfassung

  • Dark Mode: Systemfarben und Asset-Farben mit Dunkel-Variante passen sich automatisch an. @Environment(\.colorScheme) nur, wenn nötig.
  • Dynamic Type: Textstile verwenden, @ScaledMetric für Abstände und Bildgrößen.
  • Bei großen Schriften Layout umstellen mit dynamicTypeSize.isAccessibilitySize und AnyLayout oder ViewThatFits.
  • Materialien wie .regularMaterial für halbdurchsichtige, lesbare Hintergründe.
  • Liquid Glass: Standard-Komponenten bekommen es automatisch. Eigene Elemente mit .glassEffect, Buttons mit .glass und .glassProminent, Gruppen in GlassEffectContainer.
  • Glas für Bedienelemente, nicht für Inhalte.
  • Immer in allen Einstellungen testen: Variants im Canvas und Environment Overrides.

Spickzettel

Swift
@Environment(\.colorScheme) private var colorScheme
.preferredColorScheme(.dark)                        // nur mit gutem Grund

@ScaledMetric private var size = 44.0
@ScaledMetric(relativeTo: .caption) private var gap = 4.0

@Environment(\.dynamicTypeSize) private var typeSize
let layout = typeSize.isAccessibilitySize
    ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())
layout { a; b }

.dynamicTypeSize(...DynamicTypeSize.xxxLarge)       // begrenzen
.lineLimit(1...3)

.background(.regularMaterial, in: .rect(cornerRadius: 12))

.buttonStyle(.glass)  /  .buttonStyle(.glassProminent)
.glassEffect(.regular.tint(.orange).interactive(), in: .capsule)
GlassEffectContainer(spacing: 20) { ... }

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.