8  H8: Data beheren

Tot nu toe halen we data op en gebruiken die direct in één view. Maar een echte app heeft meerdere schermen die allemaal dezelfde data nodig hebben: de klok, de prijslijst, de grafiek. We willen niet voor elk scherm apart data ophalen. In dit hoofdstuk leer je hoe je een centrale databeheerder bouwt die alle schermen bedient.

8.1 Wat gaan we bouwen?

De EnergyDataManager: een @Observable-klasse die data ophaalt, opslaat en deelt met alle views. Elke view die data nodig heeft, vraagt het simpelweg op bij de manager — en wordt automatisch bijgewerkt als de data verandert.

Diagram: EnergyDataManager in het midden met pijlen naar de klok, de prijslijst en de grafiek

Meerdere schermen die allemaal dezelfde databeheerder gebruiken

8.2 Playground-voorbeeld

import SwiftUI
import PlaygroundSupport
import Observation

// Een eenvoudige databeheerder
@Observable
class TellerManager {
    var teller = 0
    var naam = "onbekend"

    func verhoog() {
        teller += 1
    }
}

// View A gebruikt de manager
struct ViewA: View {
    @Environment(TellerManager.self) var manager

    var body: some View {
        VStack {
            Text("Teller: \(manager.teller)")
            Button("Plus") { manager.verhoog() }
                .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

// View B gebruikt dezelfde manager
struct ViewB: View {
    @Environment(TellerManager.self) var manager

    var body: some View {
        Text("Ook hier: \(manager.teller)")
            .padding()
    }
}

// Hoofdview geeft de manager door via Environment
struct HoofdView: View {
    @State private var manager = TellerManager()

    var body: some View {
        VStack(spacing: 20) {
            ViewA()
            ViewB()
        }
        .environment(manager)
        .frame(width: 200, height: 150)
    }
}

PlaygroundPage.current.setLiveView(HoofdView())

Klik op “Plus” in ViewA — ViewB werkt ook bij. Één manager, twee views, automatisch gesynchroniseerd.

8.3 Concept uitgelegd

8.3.1 @Observable: een slimme klas

Een struct kun je niet delen tussen meerdere views — elke kopie is onafhankelijk. Daarvoor gebruik je een class. Met @Observable weet SwiftUI precies welke eigenschappen een view gebruikt, en tekent hij die view opnieuw als zo’n eigenschap verandert.

@Observable
class Winkelwagen {
    var producten: [String] = []
    var totaal: Double = 0
}

8.3.2 @Environment: de manager doorgeven

Je kunt de manager doorgeven via @Environment. Dat werkt als een gedeelde kast waaruit alle views kunnen pakken wat ze nodig hebben:

// In de hoofdview: stop de manager in de kast
.environment(manager)

// In een childview: pak de manager uit de kast
@Environment(Winkelwagen.self) var winkelwagen

8.3.3 @MainActor: altijd op de hoofdthread

UI-updates mogen alleen op de hoofdthread. Door een klasse te markeren met @MainActor zorgt Swift ervoor dat alle aanpassingen aan die klasse automatisch op de hoofdthread plaatsvinden.

@MainActor
@Observable
class EnergyDataManager {
    // Alles hier wordt uitgevoerd op de hoofdthread
}

8.4 Code schrijven

Stap 1: de klasse opzetten

Maak EnergyDataManager.swift:

import SwiftUI

// Bron: EnergyClock/EnergyDataManager.swift
// Coordinator: verbindt de API-client en persistentie-laag met de UI
@MainActor
@Observable
class EnergyDataManager {
    var energyPrices: [EnergyPrice] = []
    var isLoading = false
    var errorMessage: String?

    private let api = EnergyAPIClient()

    func laadPrijzen() async {
        isLoading = true
        errorMessage = nil
        do {
            energyPrices = try await api.laadHuidigePrijzen()
        } catch {
            errorMessage = error.localizedDescription
            energyPrices = maakVoorbeeldData()
        }
        isLoading = false
    }

    private func maakVoorbeeldData() -> [EnergyPrice] {
        let kalender = Calendar.current
        let vandaag = kalender.startOfDay(for: Date())
        return (0..<24).map { uur in
            let datum = kalender.date(byAdding: .hour, value: uur, to: vandaag) ?? vandaag
            return EnergyPrice(hour: uur, price: Double.random(in: 0.05...0.40), date: datum)
        }
    }
}

Stap 2: de manager in de app injecteren

Open EnergyClockApp.swift en voeg de manager toe als @State:

import SwiftUI

@main
struct EnergyClockApp: App {
    @State private var manager = EnergyDataManager()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(manager)
        }
    }
}

Stap 3: de manager gebruiken in views

In ContentView.swift:

import SwiftUI

struct ContentView: View {
    @Environment(EnergyDataManager.self) var manager

    var body: some View {
        VStack {
            if manager.isLoading {
                ProgressView("Laden...")
            } else if let fout = manager.errorMessage {
                Text("Fout: \(fout)")
                    .foregroundStyle(.red)
            } else {
                Text("\(manager.energyPrices.count) prijzen geladen")
            }
        }
        .task {
            await manager.laadPrijzen()
        }
    }
}

Stap 4: automatisch bijwerken bij verandering

Voeg didSet toe zodat de kleurmapper automatisch herberekend wordt als de prijzen veranderen:

// Bron: EnergyClock/EnergyDataManager.swift
var energyPrices: [EnergyPrice] = [] {
    didSet { herberekeningColorMapper() }
}

private(set) var colorMapper: EnergyColorMapper = EnergyColorMapper(
    prices: [],
    mode: .absolute,
    absoluteThresholds: .init(greenMax: 0.10, yellowMax: 0.20, orangeMax: 0.30)
)

private func herberekeningColorMapper() {
    colorMapper = EnergyColorMapper(
        prices: energyPrices,
        mode: .absolute,
        absoluteThresholds: .init(greenMax: 0.10, yellowMax: 0.20, orangeMax: 0.30)
    )
}
NoteVerdieping: @Observable vs. ObservableObject

Vóór iOS 17 gebruikte Swift ObservableObject met @Published-eigenschappen. @Observable (geïntroduceerd in Swift 5.9) is de moderne vervanging.

Het grote verschil: ObservableObject stuurt een signaal als iets verandert in het object, waarna SwiftUI alle views die het object gebruiken opnieuw tekent — ook al veranderde alleen een eigenschap die die view niet gebruikt. Dit heet over-invalidation en kost onnodig rekenkracht.

@Observable is slimmer: SwiftUI houdt bij welke specifieke eigenschappen een view leest tijdens het tekenen. Alleen als één van díé eigenschappen verandert, tekent SwiftUI die view opnieuw. Dit heet fine-grained dependency tracking.

Het resultaat is dat apps met @Observable sneller en zuiniger zijn, zeker bij complexere datamodellen.

8.5 Apple documentatie

Meer over @Observable:

developer.apple.com/documentation/observation

Meer over @Environment:

developer.apple.com/documentation/swiftui/environment

Meer over @MainActor:

developer.apple.com/documentation/swift/mainactor

Zoek in de Observation-documentatie naar het voorbeeld “Conforming a type to Observable”. Dat laat precies zien hoe weinig code je nodig hebt.

8.6 Samenvatting

Begrip Betekenis
class Een referentietype: gedeeld tussen views
struct Een waardetype: elke kopie is onafhankelijk
@Observable Markeert een klasse zodat SwiftUI veranderingen detecteert
@MainActor Zorgt dat alle code op de hoofdthread draait
@Environment Haalt een gedeeld object op uit de omgeving
.environment() Injecteert een object in de omgeving
didSet Voert code uit na het aanpassen van een eigenschap
ProgressView Een laadindicator

8.7 Opdracht

Breid de EnergyDataManager uit:

  1. Voeg een eigenschap lastUpdateDate: Date? toe die bijhoudt wanneer de data voor het laatste geladen is.
  2. Zet lastUpdateDate op de huidige tijd aan het einde van laadPrijzen().
  3. Toon in de ContentView de updatetijd als de data geladen is: “Bijgewerkt om 14:32”. Gebruik .formatted(date: .omitted, time: .shortened) om de datum op te maken.