9  H9: Opslaan en laden

Als je de EnergyClock sluit en opnieuw opent, moeten de prijzen er nog staan. Elke keer opnieuw laden is traag en verbruikt onnodig data. In dit hoofdstuk leer je hoe je gegevens bewaart met UserDefaults, zodat de app onthoud wat er de laatste keer geladen was.

9.1 Wat gaan we bouwen?

De EnergyPersistence-struct: een oplaglaag die prijzen, instellingen en de laatste updatetijd bewaart. Na dit hoofdstuk start de app razendsnel op met de vorige data, en laadt nieuwe data alleen als het nodig is.

Diagram: app opstart, leest data uit UserDefaults, toont klok direct

De app start snel op met gecachte data

9.2 Playground-voorbeeld

import Foundation

// Sla een waarde op
UserDefaults.standard.set("Emma", forKey: "gebruikersnaam")
UserDefaults.standard.set(42, forKey: "highscore")
UserDefaults.standard.set(true, forKey: "geluidsAan")

// Lees de waarden terug
let naam = UserDefaults.standard.string(forKey: "gebruikersnaam") ?? "onbekend"
let score = UserDefaults.standard.integer(forKey: "highscore")
let geluid = UserDefaults.standard.bool(forKey: "geluidsAan")

print(naam)    // Emma
print(score)   // 42
print(geluid)  // true

Sluit Playgrounds, open het opnieuw — de waarden zijn nog steeds opgeslagen. Dat is UserDefaults.

9.3 Concept uitgelegd

9.3.1 UserDefaults: de brievenbus van je app

UserDefaults is als een brievenbus: je stopt er iets in, en het ligt er de volgende dag nog. Perfect voor kleine hoeveelheden data: instellingen, de laatste keuze van de gebruiker, een datum.

Je slaat iets op met een sleutel — een naam die je zelf kiest. Later gebruik je diezelfde sleutel om de waarde terug te lezen.

// Opslaan
UserDefaults.standard.set(14, forKey: "aantalUren")

// Lezen
let uren = UserDefaults.standard.integer(forKey: "aantalUren")

9.3.2 Codable structs opslaan

Eenvoudige types (String, Int, Bool, Date) kun je direct opslaan. Maar voor een lijst van EnergyPrice-objecten moet je eerst encoderen naar Data, en bij het lezen weer decoderen:

// Opslaan
let encoder = JSONEncoder()
if let data = try? encoder.encode(mijnPrijzen) {
    UserDefaults.standard.set(data, forKey: "opgeslagenPrijzen")
}

// Laden
if let data = UserDefaults.standard.data(forKey: "opgeslagenPrijzen"),
   let prijzen = try? JSONDecoder().decode([EnergyPrice].self, from: data) {
    print("Geladen: \(prijzen.count) prijzen")
}

9.3.3 Sleutels centraal bewaren

Als je sleutels als losse strings overal in de code zet, vergeet je ze makkelijk of maak je een typfout. Zet ze op één plek in een enum:

enum OpslagSleutel {
    static let prijzen     = "savedEnergyPrices"
    static let updateDatum = "lastUpdateDate"
}

9.4 Code schrijven

Stap 1: de sleutels

Maak EnergyPersistence.swift en begin met de sleutels:

import Foundation

// Bron: EnergyClock/EnergyPersistence.swift
// Alle UserDefaults sleutels op één plek
enum OpslagSleutel {
    static let prijzen     = "savedEnergyPrices"
    static let bron        = "selectedEnergySource"
    static let updateDatum = "lastUpdateDate"
    static let eigenURL    = "customURL"
}

Stap 2: de persistentie-struct

// Bron: EnergyClock/EnergyPersistence.swift
// Laadt en slaat alle app-instellingen en prijsdata op
struct EnergyPersistence {
    private let defaults = UserDefaults.standard

    // MARK: - Opslaan

    func slaOp(prijzen: [EnergyPrice], updateDatum: Date) {
        if let encoded = try? JSONEncoder().encode(prijzen) {
            defaults.set(encoded, forKey: OpslagSleutel.prijzen)
        }
        defaults.set(updateDatum, forKey: OpslagSleutel.updateDatum)
    }

    func slaOp(eigenURL: String) {
        defaults.set(eigenURL, forKey: OpslagSleutel.eigenURL)
    }

    // MARK: - Laden

    func laadPrijzen() -> [EnergyPrice] {
        guard let data = defaults.data(forKey: OpslagSleutel.prijzen),
              let decoded = try? JSONDecoder().decode([EnergyPrice].self, from: data) else {
            return []
        }
        return decoded
    }

    func laadUpdateDatum() -> Date? {
        defaults.object(forKey: OpslagSleutel.updateDatum) as? Date
    }

    func laadEigenURL() -> String {
        defaults.string(forKey: OpslagSleutel.eigenURL) ?? ""
    }
}

Stap 3: de manager koppelen aan de opslag

Voeg opslag toe aan EnergyDataManager:

// Bron: EnergyClock/EnergyDataManager.swift
private let opslag = EnergyPersistence()

init() {
    // Laad opgeslagen data direct bij opstarten
    energyPrices   = opslag.laadPrijzen()
    lastUpdateDate = opslag.laadUpdateDatum()
}

func laadPrijzen() async {
    isLoading = true
    errorMessage = nil
    do {
        energyPrices = try await api.laadHuidigePrijzen()
        let updateDatum = Date()
        lastUpdateDate = updateDatum
        // Sla de nieuwe data op
        opslag.slaOp(prijzen: energyPrices, updateDatum: updateDatum)
    } catch {
        errorMessage = error.localizedDescription
        energyPrices = opslag.laadPrijzen()  // gebruik gecachte data als fallback
    }
    isLoading = false
}

Stap 4: alleen laden als nodig

// Bron: EnergyClock/EnergyDataManager.swift
func laadBijOpstarten() async {
    // Controleer of de opgeslagen data van vandaag is
    let dataIsActueel = lastUpdateDate.map {
        Calendar.current.isDateInToday($0)
    } ?? false

    if !dataIsActueel {
        await laadPrijzen()
    }
    // Anders: gebruik de al gecachte data
}
NoteVerdieping: UserDefaults en de App Sandbox

Apps op macOS en iOS draaien in een sandbox: een afgeschermde omgeving zonder toegang tot bestanden van andere apps. UserDefaults slaat gegevens op in een .plist-bestand binnen die sandbox, op een locatie als ~/Library/Containers/com.jouwnaam.app/Data/Library/Preferences/.

Voor de widget (zie H13) moeten de app en widget dezelfde data lezen. Dat gaat via een App Group: een gedeeld stukje opslagruimte waar beide toegang toe hebben. In plaats van UserDefaults.standard gebruik je dan:

UserDefaults(suiteName: "group.com.jouwnaam.app")

Beide de app én de widget moeten dezelfde App Group ID hebben in hun Entitlements, en de groep moet aangemaakt zijn in je Apple Developer account.

UserDefaults is niet geschikt voor grote hoeveelheden data. Voor meer dan een paar megabyte gebruik je FileManager om naar een bestand te schrijven, of SwiftData voor een database.

9.5 Apple documentatie

Meer over UserDefaults:

developer.apple.com/documentation/foundation/userdefaults

Meer over App Groups:

developer.apple.com/documentation/xcode/configuring-app-groups

Zoek in de UserDefaults-documentatie naar set(_:forKey:) en data(forKey:). Kijk welke typen je direct kunt opslaan (de “Simple type” overzicht) en welke je eerst moet encoderen.

9.6 Samenvatting

Begrip Betekenis
UserDefaults Sla kleine hoeveelheden data op die bewaard blijven
Sleutel Een naam om een opgeslagen waarde mee terug te vinden
JSONEncoder Zet een Codable struct om naar Data
JSONDecoder Zet Data terug naar een Codable struct
App Group Gedeelde opslag tussen app en widget
try? Probeer iets; als het mislukt geeft het nil terug

9.7 Opdracht

Breid EnergyPersistence uit:

  1. Voeg een functie slaOp(kleurModus: ColorMode) toe die de kleurinstelling bewaart.
  2. Voeg een functie laadKleurModus() -> ColorMode toe die de instelling laadt, met .absolute als standaardwaarde als er nog niets opgeslagen is.
  3. Roep beide functies aan vanuit EnergyDataManager: sla de kleurinstelling op als ze verandert, en laad hem bij initialisatie.