12  H12: Instellingen

De meeste apps hebben een instellingenscherm: kies je databron, pas kleuren aan, stel een voorkeur in. In dit hoofdstuk bouw je een instellingenscherm met tabbladen, pickers, tekstvelden en steppers — en je leert hoe je @Bindable gebruikt om instellingen direct door te schrijven naar de databeheerder.

12.1 Wat gaan we bouwen?

De SettingsView: een instellingenscherm met twee tabbladen — “Databron” en “Weergave”. De gebruiker kan de API-bron kiezen, drempelwaarden aanpassen en het thema instellen.

Het instellingenscherm met tabbladen voor Databron en Weergave

Het instellingenscherm met twee tabbladen

12.2 Playground-voorbeeld

import SwiftUI
import PlaygroundSupport
import Observation

@Observable
class Instellingen {
    var naam = "Mijn app"
    var aantalDagen = 7
    var geluidsAan = true
}

struct InstellingenView: View {
    @Bindable var instellingen: Instellingen

    var body: some View {
        Form {
            TextField("Naam", text: $instellingen.naam)
            Stepper("Dagen: \(instellingen.aantalDagen)", value: $instellingen.aantalDagen, in: 1...30)
            Toggle("Geluid", isOn: $instellingen.geluidsAan)
        }
        .formStyle(.grouped)
        .frame(width: 300, height: 200)
    }
}

let inst = Instellingen()
PlaygroundPage.current.setLiveView(InstellingenView(instellingen: inst))

Pas een waarde aan — de Instellingen-klasse wordt direct bijgewerkt.

12.3 Concept uitgelegd

12.3.1 @Bindable: twee-richtingsverkeer

In H2 leerde je @State voor lokale waarden. Met @Bindable kun je twee-richtingsverbindingen maken met een @Observable-object:

@Bindable var manager: EnergyDataManager

// Nu kun je $manager.colorMode gebruiken in een Picker
Picker("Kleurmodus", selection: $manager.colorMode) { ... }

Het $-teken maakt een binding: als de gebruiker een keuze maakt in de Picker, schrijft die automatisch terug naar manager.colorMode.

12.3.2 Form: een instellingsformulier

Form is de standaard container voor instellingen. Hij past zijn opmaak automatisch aan het platform aan:

Form {
    Section("Geluid") {
        Toggle("Geluidseffecten", isOn: $instellingen.geluidsAan)
    }
    Section("Data") {
        Stepper("Dagen: \(aantalDagen)", value: $aantalDagen, in: 1...30)
    }
}
.formStyle(.grouped)

12.3.3 TabView met het Tab-API

Een scherm met tabbladen gebruik je zo:

// Bron: EnergyClock/SettingsView.swift
TabView {
    Tab("Databron", systemImage: "antenna.radiowaves.left.and.right") {
        DatabronInstellingen(manager: manager)
    }
    Tab("Weergave", systemImage: "paintpalette") {
        WeergaveInstellingen(manager: manager)
    }
}

Let op: gebruik altijd de Tab-API (niet .tabItem()). De Tab-API is de moderne variant.

12.4 Code schrijven

Stap 1: de buitenkant

Maak SettingsView.swift:

import SwiftUI

// Bron: EnergyClock/SettingsView.swift
struct SettingsView: View {
    @Bindable var manager: EnergyDataManager

    var body: some View {
        TabView {
            Tab("Databron", systemImage: "antenna.radiowaves.left.and.right") {
                DatabronInstellingen(manager: manager)
            }
            Tab("Weergave", systemImage: "paintpalette") {
                WeergaveInstellingen(manager: manager)
            }
        }
    }
}

Stap 2: het Databron-tabblad

// Bron: EnergyClock/SettingsView.swift – DatabronInstellingen
private struct DatabronInstellingen: View {
    @Bindable var manager: EnergyDataManager

    var body: some View {
        Form {
            Section {
                // Kies de API-bron
                Picker("Bron", selection: $manager.selectedSource) {
                    ForEach(EnergyPriceSource.allCases) { bron in
                        Text(bron.rawValue).tag(bron)
                    }
                }
                .onChange(of: manager.selectedSource) { _, nieuweBron in
                    manager.wisselNaarBronCache(nieuweBron)
                    Task { await manager.loadEnergyPrices() }
                }
            }

            Section {
                // Aantal historische dagen
                HStack {
                    Text("Prijsgeschiedenis")
                    Spacer()
                    TextField("", value: $manager.historischeDagenOpslaan, format: .number)
                        .frame(width: 48)
                        .textFieldStyle(.roundedBorder)
                        .multilineTextAlignment(.trailing)
                    Text("dagen")
                        .foregroundStyle(.secondary)
                    Stepper(
                        "",
                        value: $manager.historischeDagenOpslaan,
                        in: 1...60,
                        step: 1
                    )
                    .labelsHidden()
                }
            } footer: {
                Text("Hoeveel dagen worden opgeslagen voor de boxplot en relatieve kleuren.")
                    .font(.caption)
            }
        }
        .formStyle(.grouped)
        .frame(width: 540)
    }
}

Stap 3: het Weergave-tabblad

// Bron: EnergyClock/SettingsView.swift – WeergaveInstellingen
private struct WeergaveInstellingen: View {
    @Bindable var manager: EnergyDataManager

    var body: some View {
        Form {
            // Kleurmodus
            Section {
                Picker("Kleurmodus", selection: $manager.colorMode) {
                    ForEach(ColorMode.allCases) { modus in
                        Text(modus.rawValue).tag(modus)
                    }
                }
                .pickerStyle(.segmented)
                .onChange(of: manager.colorMode) { _, _ in
                    manager.saveColorSettings()
                }
            } header: {
                Text("Kleurmodus")
            }

            // Drempelwaarden
            if manager.colorMode == .absolute {
                Section("Drempelwaarden (EUR/kWh)") {
                    HStack {
                        Circle().fill(Color.green).frame(width: 12, height: 12)
                        Text("Groen tot")
                        Spacer()
                        TextField("", value: $manager.absoluteThresholds.greenMax,
                                  format: .number.precision(.fractionLength(2)))
                            .frame(width: 80)
                            .textFieldStyle(.roundedBorder)
                            .multilineTextAlignment(.trailing)
                            .onChange(of: manager.absoluteThresholds.greenMax) { _, _ in
                                manager.saveColorSettings()
                            }
                    }
                    // oranje en rood op dezelfde manier...
                }
            }
        }
        .formStyle(.grouped)
        .frame(width: 540)
    }
}
NoteVerdieping: @Bindable en het Observation-framework

@Bindable is onderdeel van het Observation-framework (Swift 5.9+). Het verschil met oudere benaderingen:

Met ObservableObject (oud):

@ObservedObject var manager: EnergyDataManager
// $manager.colorMode werkte via @Published

Met @Observable (nieuw):

@Bindable var manager: EnergyDataManager
// $manager.colorMode werkt via synthetische bindingen

@Bindable genereert automatisch bindingen voor alle opslagbare (var) eigenschappen van een @Observable-object. Achter de schermen gebruikt het Swift’s DynamicProperty-protocol en macros die de compiler tijdens het compileren uitbreidt.

Een binding is geen kopie van de waarde — het is een verwijzing naar de locatie in het geheugen plus een setter-functie. Als je een binding doorgeeft via $manager.colorMode, geef je de view toegang om die specifieke eigenschap te lezen en te schrijven zonder het hele object te hoeven kennen.

12.5 Apple documentatie

Meer over Form:

developer.apple.com/documentation/swiftui/form

Meer over @Bindable:

developer.apple.com/documentation/swiftui/bindable

Meer over Picker:

developer.apple.com/documentation/swiftui/picker

Zoek in de Form-documentatie naar formStyle(.grouped) — dat is de macOS/iOS-stijl die lijkt op de Instellingen-app.

12.6 Samenvatting

Begrip Betekenis
@Bindable Maakt twee-richtingsbindingen met een @Observable-object
$ Binding-operator: stuurt wijzigingen terug naar de bron
Form Container voor instellingselementen
Section Groepeert elementen in een Form
TabView Scherm met tabbladen onderaan of bovenaan
Tab Eén tabblad in een TabView
Toggle Een aan/uit-schakelaar
Stepper Verhoog of verlaag een getal met knoppen
Picker Kies één optie uit een lijst
.onChange(of:) Voert code uit als een waarde verandert

12.7 Opdracht

Voeg een derde tabblad toe aan SettingsView: “Info”. Dit tabblad toont:

  1. De versie van de app (gebruik Bundle.main.infoDictionary?["CFBundleShortVersionString"]).
  2. Een link-knop naar de GitHub-pagina van het project.
  3. Een knop “Cache wissen” die alle opgeslagen prijzen verwijdert uit UserDefaults (waarschuwing: de app laadt daarna nieuwe data).