Lektion 3.13 · Stufe 3 – Fortgeschritten

Einstellungen speichern mit @AppStorage

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du kleine Werte wie Einstellungen mit @AppStorage dauerhaft speichern, sodass sie einen Neustart der App überleben. Du kennst die Grenzen dieses Speichers, speicherst auch Enums und einfache Structs und kennst @SceneStorage für den Zustand eines Fensters.

Das Problem: Alles vergessen

Starte eine deiner bisherigen Apps, ändere eine Einstellung und beende die App ganz. Beim nächsten Start ist die Einstellung wieder weg. Der Habit-Tracker hat seine Gewohnheiten vergessen, die Rezept-App ihre Favoriten.

Der Grund: @State und @Observable-Objekte leben nur im Arbeitsspeicher. Beendet iOS die App, ist dieser Speicher weg. Für Daten, die bleiben sollen, brauchst du einen dauerhaften Speicher auf dem Gerät.

Für große Datenmengen wie Hunderte Gewohnheiten oder Ausgaben gibt es SwiftData (ab der nächsten Lektion). Für kleine Werte wie Einstellungen gibt es etwas viel Einfacheres.

@AppStorage

Swift
struct SettingsView: View {
    @AppStorage("userName") private var userName = ""
    @AppStorage("notificationsOn") private var notificationsOn = true
    @AppStorage("dailyGoal") private var dailyGoal = 3

    var body: some View {
        Form {
            TextField("Dein Name", text: $userName)
            Toggle("Benachrichtigungen", isOn: $notificationsOn)
            Stepper("Tagesziel: \(dailyGoal)", value: $dailyGoal, in: 1...10)
        }
    }
}

Das sieht fast genauso aus wie @State, mit einem Unterschied: Die Werte werden auf dem Gerät gespeichert. Beende die App, starte sie neu, und alles ist noch da.

Erwartetes Ergebnis

Gib einen Namen ein, schalte die Benachrichtigungen aus und erhöhe das Tagesziel. Beende die App im Simulator ganz (im App-Umschalter nach oben wischen) und starte sie neu. Alle Werte sind noch da.

So funktioniert es:

  • "userName" ist der Schlüssel, unter dem der Wert gespeichert wird. Er muss in der ganzen App eindeutig sein.
  • = "" ist der Standardwert, solange noch nichts gespeichert wurde.
  • $userName liefert ein Binding (Lektion 2.10), genau wie bei @State.
  • Ändert sich der Wert, wird er sofort gespeichert, und alle Views, die denselben Schlüssel verwenden, aktualisieren sich.

Ein Bild für @AppStorage

Stell dir einen kleinen Zettelkasten vor, der fest zur App gehört. Jeder Zettel hat eine Beschriftung, den Schlüssel, und einen Wert. @AppStorage liest beim Start den passenden Zettel und schreibt bei jeder Änderung einen neuen.

Was gespeichert werden kann

@AppStorage funktioniert mit einfachen Typen:

TypBeispiel
StringName, Sprache
Int, DoubleZiele, Zähler, Größen
BoolSchalter
URLeine gespeicherte Adresse
Databeliebige Daten in Rohform
Optionals dieser TypenString? für „noch nicht gesetzt“
DateZeitpunkte (in neueren iOS-Versionen)

Und, besonders praktisch, Enums mit String- oder Int-Raw-Value:

Swift
enum AppTheme: String, CaseIterable, Identifiable {
    case system = "System"
    case light = "Hell"
    case dark = "Dunkel"

    var id: Self { self }

    var colorScheme: ColorScheme? {
        switch self {
        case .system: nil
        case .light: .light
        case .dark: .dark
        }
    }
}

struct ThemePicker: View {
    @AppStorage("theme") private var theme = AppTheme.system

    var body: some View {
        Picker("Design", selection: $theme) {
            ForEach(AppTheme.allCases) { theme in
                Text(theme.rawValue)
            }
        }
    }
}

Gespeichert wird der Raw Value, also "Hell". Beim Lesen macht @AppStorage daraus wieder AppTheme.light. Gibt es den gespeicherten Wert nicht mehr, etwa weil du einen Fall umbenannt hast, wird der Standardwert verwendet.

Die Einstellung anwenden

Damit das gewählte Design die ganze App betrifft, liest du es in der App-Struct:

Swift
@main
struct MyApp: App {
    @AppStorage("theme") private var theme = AppTheme.system

    var body: some Scene {
        WindowGroup {
            ContentView()
                .preferredColorScheme(theme.colorScheme)
        }
    }
}

.preferredColorScheme(nil) heißt: „Folge der Systemeinstellung.“ Ändert der Nutzer das Design in den Einstellungen deiner App, wechselt die ganze App sofort. Beide Stellen verwenden denselben Schlüssel "theme" und bleiben so automatisch synchron.

Schlüssel an einer Stelle sammeln

Schlüssel sind einfache Texte. Vertippst du dich an einer Stelle, "theme" hier und "Theme" dort, gibt es zwei verschiedene Einträge, und nichts passt zusammen. Kein Fehler, keine Warnung.

Sammle die Schlüssel deshalb an einer Stelle:

Swift
enum StorageKey {
    static let theme = "theme"
    static let userName = "userName"
    static let dailyGoal = "dailyGoal"
}

@AppStorage(StorageKey.theme) private var theme = AppTheme.system

Jetzt meldet Xcode einen Tippfehler sofort.

Wo @AppStorage gespeichert wird

Hinter @AppStorage steckt UserDefaults, ein einfacher Speicher, den iOS jeder App zur Verfügung stellt. Er ist gedacht für kleine Einstellungen und wird beim App-Start komplett in den Arbeitsspeicher geladen.

Was nicht in @AppStorage gehört

  • Große Datenmengen: Listen mit Hunderten Einträgen, Bilder, Dokumente. Dafür gibt es SwiftData oder Dateien.
  • Vertrauliches: Passwörter, Zugangsdaten, Tokens. UserDefaults ist nicht verschlüsselt. Dafür gibt es die Keychain.
  • Daten, die oft und schnell ändern, etwa jeder Zwischenstand einer Animation.

Faustregel: Wenn es in den Einstellungen einer App stehen könnte, passt es in @AppStorage.

Einfache Structs speichern

Manchmal willst du eine kleine Struct speichern, etwa eine Liste von Favoriten-IDs. @AppStorage kann Data speichern, und mit Codable (Lektion 3.9) wird aus einer Struct Data. Ein bewährtes Muster ist eine berechnete Eigenschaft, die beides verbindet:

Swift
struct FavoritesView: View {
    @AppStorage("favoriteIDs") private var favoriteData = Data()

    private var favoriteIDs: Set<String> {
        get { (try? JSONDecoder().decode(Set<String>.self, from: favoriteData)) ?? [] }
        nonmutating set { favoriteData = (try? JSONEncoder().encode(newValue)) ?? Data() }
    }

    var body: some View {
        Text("\(favoriteIDs.count) Favoriten")
        Button("Hinzufügen") {
            var ids = favoriteIDs
            ids.insert(UUID().uuidString)
            favoriteIDs = ids
        }
    }
}
  • Der Getter dekodiert die gespeicherten Daten, mit leerem Set als Ersatz.
  • Der Setter kodiert den neuen Wert und speichert ihn. nonmutating erlaubt das Setzen in einer View-Struct, weil @AppStorage den Wert ohnehin außerhalb der Struct speichert.
  • Set<String> ist automatisch Codable.

Tipp

Für ein kleines Set aus IDs ist das völlig in Ordnung. Wird es größer oder komplexer, ist SwiftData die bessere Wahl.

@AppStorage in einer @Observable-Klasse

@AppStorage ist für Views gedacht. In einer @Observable-Klasse funktioniert es nicht direkt. Brauchst du gespeicherte Werte in einem Store, liest und schreibst du UserDefaults selbst:

Swift
@Observable
class FavoritesStore {
    private(set) var ids: Set<String> {
        didSet { save() }
    }

    init() {
        let saved = UserDefaults.standard.stringArray(forKey: "favoriteIDs") ?? []
        ids = Set(saved)
    }

    func toggle(id: String) {
        if ids.contains(id) { ids.remove(id) } else { ids.insert(id) }
    }

    private func save() {
        UserDefaults.standard.set(Array(ids), forKey: "favoriteIDs")
    }
}
  • didSet läuft nach jeder Änderung der Eigenschaft und speichert sofort.
  • Im init wird der gespeicherte Stand geladen. didSet läuft dabei nicht, weil Eigenschaften im eigenen Initializer ohne Beobachter gesetzt werden.
  • UserDefaults kann Arrays von Strings direkt speichern. Ein Set wird dafür in ein Array umgewandelt.

Damit überleben die Favoriten aus Projekt D jetzt einen Neustart.

@SceneStorage: Wo war ich?

Es gibt noch einen zweiten, ähnlichen Property Wrapper: @SceneStorage. Er speichert Werte nicht für die ganze App, sondern für ein Fenster (eine Scene, Lektion 2.2), und zwar nur so lange, wie iOS dieses Fenster wiederherstellen kann.

Swift
@SceneStorage("selectedTab") private var selectedTab = "home"
@SceneStorage("draftText") private var draft = ""

Typisch ist das für den Bedienzustand: Welcher Tab war offen? Was stand halb getippt im Notizfeld? Beendet iOS die App im Hintergrund, um Speicher freizugeben, und der Nutzer kehrt zurück, findet er alles so vor, wie er es verlassen hat.

@AppStorage@SceneStorage
Gilt fürdie ganze Appein Fenster
Hältdauerhaftbis das Fenster geschlossen wird
Typisch fürEinstellungengewählter Tab, Entwürfe, Scrollposition

Typische Fehler

Zwei verschiedene Schlüssel

@AppStorage("dailyGoal") hier, @AppStorage("dailygoal") dort. Die Werte sind nicht verbunden. Sammle Schlüssel in einem Enum.

Unterschiedliche Standardwerte

Zwei Views lesen denselben Schlüssel mit verschiedenen Standardwerten, etwa = 3 und = 5. Solange nichts gespeichert ist, zeigen sie unterschiedliche Werte. Leg Standardwerte ebenfalls zentral fest.

Werte im Simulator „hängen fest“

Beim Testen willst du manchmal von vorn anfangen. Lösch die App im Simulator (lange auf das Symbol drücken → App entfernen). Dann ist auch UserDefaults leer.

Struct direkt in @AppStorage

Swift
@AppStorage("profile") private var profile = Profile()

Fehlermeldung

No exact matches in call to initializer

@AppStorage unterstützt nur einfache Typen. Speichere die Struct als Data wie oben gezeigt.

FAQ

Anfänger

Was bedeutet das überhaupt?

Was passiert mit den Daten, wenn der Nutzer die App löscht? Sie werden mitgelöscht. Bei einer Neuinstallation beginnt alles mit den Standardwerten.

Werden @AppStorage-Werte zwischen iPhone und iPad synchronisiert? Nein, sie bleiben auf dem Gerät. Für die Synchronisation über iCloud gibt es NSUbiquitousKeyValueStore oder SwiftData mit CloudKit.

Wie viel darf ich in @AppStorage speichern? Es gibt keine harte Grenze, aber UserDefaults ist für einige Kilobyte gedacht. Bleib bei Einstellungen und kleinen Listen.

Fortgeschritten

Warum verhält es sich so?

Kann ich @AppStorage mit einer App Group teilen, etwa mit einem Widget? Ja, mit @AppStorage("key", store: UserDefaults(suiteName: "group.com.deinname.app")). App und Widget müssen dafür derselben App Group angehören, einstellbar unter Signing & Capabilities. Das brauchst du in Lektion 4.18.

Wie lege ich Standardwerte für viele Schlüssel zentral fest? Mit UserDefaults.standard.register(defaults: ["dailyGoal": 3, …]) beim Start der App. Diese Werte gelten, solange nichts anderes gespeichert ist. @AppStorage übernimmt dann aber trotzdem seinen eigenen Standardwert aus dem Code, achte also darauf, dass beide übereinstimmen.

Profi

Was passiert unter der Haube?

Wann genau schreibt UserDefaults auf die Festplatte? Änderungen landen sofort im Arbeitsspeicher und werden vom System gebündelt und zeitnah gesichert. Früher gab es dafür synchronize(), das heute nicht mehr nötig ist und in Apples Dokumentation als veraltet markiert ist.

Wie bemerkt @AppStorage Änderungen von anderer Stelle? @AppStorage beobachtet UserDefaults auf Änderungen an seinem Schlüssel. Schreibt anderer Code direkt mit UserDefaults.standard.set(...) unter diesen Schlüssel, aktualisiert sich die View trotzdem. Umgekehrt bekommt eine @Observable-Klasse Änderungen aus einer View mit @AppStorage nicht automatisch mit. Halte jeden Wert deshalb möglichst an einer Stelle.

Übungen

Anfänger

Übung 1: Onboarding nur einmal

Zeig beim ersten Start der App einen Willkommensbildschirm mit einem Button „Los geht's“. Danach soll er nie wieder erscheinen. Speichere dafür mit @AppStorage einen Bool hasSeenOnboarding.

Lösung Übung 1Erst selbst probieren!
Swift
struct RootView: View {
    @AppStorage("hasSeenOnboarding") private var hasSeenOnboarding = false

    var body: some View {
        if hasSeenOnboarding {
            ContentView()
        } else {
            VStack(spacing: 24) {
                Text("Willkommen!")
                    .font(.largeTitle).bold()
                Button("Los geht's") {
                    hasSeenOnboarding = true
                }
                .buttonStyle(.borderedProminent)
            }
        }
    }
}
Fortgeschritten

Übung 2: Einstellungen für den Habit-Tracker

Ergänze den Habit-Tracker aus Projekt C um eine Einstellungsseite: Design (System/Hell/Dunkel), Streaks anzeigen (ja/nein) und ein Tagesziel (1–10). Alles wird mit @AppStorage gespeichert, und das Design wirkt auf die ganze App.

Profi

Übung 3: Favoriten dauerhaft

Stell den FavoritesStore aus Projekt D auf UserDefaults um, wie in dieser Lektion gezeigt. Prüf, ob die Favoriten einen Neustart überleben. Was passiert, wenn du in der Simulator-Einstellung die App löschst und neu installierst?

Zusammenfassung

  • @AppStorage("schlüssel") speichert kleine Werte dauerhaft auf dem Gerät und funktioniert wie @State.
  • Unterstützt einfache Typen, Optionals und Enums mit Raw Value.
  • Schlüssel zentral sammeln, um Tippfehler zu vermeiden.
  • Dahinter steckt UserDefaults: für Einstellungen, nicht für große Daten oder Passwörter.
  • Structs über Codable und Data speichern, in Klassen UserDefaults direkt verwenden.
  • @SceneStorage merkt sich den Bedienzustand eines Fensters.

Spickzettel

Swift
@AppStorage("userName") private var userName = ""
@AppStorage("goal") private var goal = 3
@AppStorage("theme") private var theme = AppTheme.system   // Enum mit Raw Value
@SceneStorage("selectedTab") private var tab = "home"

TextField("Name", text: $userName)
ContentView().preferredColorScheme(theme.colorScheme)

UserDefaults.standard.set(value, forKey: "key")
UserDefaults.standard.string(forKey: "key")

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.