10  H10: Prijslijst

De klok is mooi, maar soms wil je gewoon een lijstje zien: uur voor uur, wat kost de stroom? In dit hoofdstuk bouw je een prijslijst die de uren groepeert per dag, het huidige uur markeert, en netjes omgaat met ontbrekende data.

10.1 Wat gaan we bouwen?

De PriceSidebarView: een scrollbare lijst van uurprijzen, gegroepeerd per dag. Het huidige uur is gemarkeerd. Als data ontbreekt, ziet de gebruiker een duidelijke melding.

Een scrollbare lijst van prijzen per uur, met vandaag en morgen als aparte secties

De prijslijst met uren gegroepeerd per dag

10.2 Playground-voorbeeld

import SwiftUI
import PlaygroundSupport

struct FruitLijst: View {
    let fruit = ["Appel", "Banaan", "Kers", "Druif", "Peer"]

    var body: some View {
        List(fruit, id: \.self) { naam in
            HStack {
                Circle()
                    .fill(.green)
                    .frame(width: 10, height: 10)
                Text(naam)
            }
        }
        .frame(width: 200, height: 200)
    }
}

PlaygroundPage.current.setLiveView(FruitLijst())

Een lijst van fruit. Elke rij heeft een groen bolletje en een naam. Zo werkt List in SwiftUI.

10.3 Concept uitgelegd

10.3.1 List en ForEach: lijsten bouwen

List is de standaard manier om een lijst te tonen in SwiftUI. Je geeft hem een array en een manier om elk item te tonen:

List(prijzen, id: \.id) { prijs in
    Text("\(prijs.hour):00")
}

ForEach werkt hetzelfde maar kan ook buiten een List gebruikt worden — in een VStack of ScrollView:

ScrollView {
    LazyVStack {
        ForEach(prijzen) { prijs in
            Text("\(prijs.hour):00")
        }
    }
}

LazyVStack is een slimme versie van VStack: hij maakt rijen pas aan als ze zichtbaar worden. Handig bij lange lijsten.

10.3.2 Groeperen per dag

Om prijzen te groeperen per dag gebruik je Dictionary(grouping:by:):

let gegroepeerd = Dictionary(grouping: prijzen) { prijs in
    Calendar.current.startOfDay(for: prijs.date)
}
// gegroepeerd is nu [Date: [EnergyPrice]]

Dit geeft een woordenboek met als sleutel de begintijd van de dag, en als waarde de prijzen van die dag.

10.3.3 Het huidige uur markeren

Je kunt het huidige uur markeren door te checken of de datum van een rij overeenkomt met het huidige uur:

var isHuidigUur: Bool {
    Calendar.current.isDate(prijs.date, equalTo: Date(), toGranularity: .hour)
}

10.4 Code schrijven

Stap 1: de prijsrij

Maak een aparte struct voor één rij in de lijst:

import SwiftUI

// Bron: EnergyClock/PriceSidebarView.swift – PriceRow
struct PrijsRij: View {
    let prijs: EnergyPrice
    let colorMapper: EnergyColorMapper

    var isHuidigUur: Bool {
        Calendar.current.isDate(prijs.date, equalTo: Date(), toGranularity: .hour)
    }

    var body: some View {
        HStack {
            // Tijd als tekst
            Text(prijs.date, format: .dateTime.hour().minute())
                .font(.system(.body, design: .monospaced))
                .frame(width: 80, alignment: .leading)

            Spacer()

            // Gekleurde stip
            Circle()
                .fill(colorMapper.color(for: prijs))
                .frame(width: 8, height: 8)

            // Prijs of streepje
            if let bedrag = prijs.price {
                Text(bedrag, format: .number.precision(.fractionLength(2)))
                    .font(.system(.body, design: .monospaced))
                    .frame(width: 70, alignment: .trailing)
            } else {
                Text("–")
                    .foregroundStyle(.secondary)
                    .frame(width: 70, alignment: .trailing)
            }
        }
        .padding(.horizontal)
        .padding(.vertical, 8)
        .background(isHuidigUur ? Color.accentColor.opacity(0.15) : Color.clear)
    }
}

Stap 2: de volledige lijstview

import SwiftUI

// Bron: EnergyClock/PriceSidebarView.swift
struct PrijsLijstView: View {
    let energyPrices: [EnergyPrice]
    let colorMapper: EnergyColorMapper

    // Bereken groepen één keer in init, niet bij elke render
    let prijzenPerDag: [(datum: Date, prijzen: [EnergyPrice])]

    init(energyPrices: [EnergyPrice], colorMapper: EnergyColorMapper) {
        self.energyPrices = energyPrices
        self.colorMapper = colorMapper

        let gesorteerd = energyPrices.sorted { $0.date < $1.date }
        let kalender = Calendar.current
        let gegroepeerd = Dictionary(grouping: gesorteerd) { prijs in
            kalender.startOfDay(for: prijs.date)
        }
        self.prijzenPerDag = gegroepeerd
            .sorted { $0.key < $1.key }
            .map { (datum: $0.key, prijzen: $0.value.sorted { $0.date < $1.date }) }
    }

    var body: some View {
        ScrollView {
            LazyVStack(spacing: 0) {
                ForEach(prijzenPerDag, id: \.datum) { groep in
                    // Dag-header
                    HStack {
                        Text(groep.datum, format: .dateTime.weekday(.wide).day().month(.wide))
                            .font(.headline)
                        Spacer()
                    }
                    .padding(.horizontal)
                    .padding(.vertical, 8)

                    Divider()

                    // Prijzen voor deze dag
                    ForEach(groep.prijzen) { prijs in
                        PrijsRij(prijs: prijs, colorMapper: colorMapper)
                        Divider()
                            .padding(.leading)
                    }
                }
            }
        }
        .background(Color(NSColor.controlBackgroundColor))
    }
}

Stap 3: lege toestand

Gebruik ContentUnavailableView als er geen data is:

var body: some View {
    if energyPrices.isEmpty {
        ContentUnavailableView(
            "Geen prijzen beschikbaar",
            systemImage: "bolt.slash",
            description: Text("Ververs om nieuwe data op te halen")
        )
    } else {
        ScrollView {
            // ... de lijst
        }
    }
}
NoteVerdieping: LazyVStack vs. List

List en LazyVStack doen op het eerste gezicht hetzelfde, maar er zijn belangrijke verschillen:

  • List heeft ingebouwde ondersteuning voor swipe-to-delete, selectie en reorder. Het heeft ook zijn eigen achtergrond en scheidingslijnen.
  • LazyVStack in een ScrollView geeft je volledige controle over de opmaak. Geen ingebouwde swipe-actions, maar ook geen ongewenste standaard stijlen.

Lazy in LazyVStack betekent dat rijen pas worden aangemaakt als ze zichtbaar worden in de ScrollView. Voor een lijst van 24 rijen maakt dit weinig verschil. Bij duizenden rijen — denk aan een transactiegeschiedenis — scheelt dit aanzienlijk in geheugengebruik en opstarttijd.

List gebruikt intern ook lazy loading, maar doet dit automatisch zonder dat je dat in de naam ziet.

10.5 Apple documentatie

Meer over List:

developer.apple.com/documentation/swiftui/list

Meer over LazyVStack:

developer.apple.com/documentation/swiftui/lazyvstack

Meer over ContentUnavailableView:

developer.apple.com/documentation/swiftui/contentunavailableview

Zoek in de List-documentatie naar “Grouping” — dat laat zien hoe je een List kunt indelen met secties en headers.

10.6 Samenvatting

Begrip Betekenis
List Een ingebouwde SwiftUI-lijstview
ForEach Herhaalt een view voor elk item in een array
LazyVStack Verticale stapel die rijen pas aanmaakt als ze zichtbaar zijn
ScrollView Maakt de inhoud scrollbaar
Dictionary(grouping:by:) Groepeert een array in een woordenboek
ContentUnavailableView Toont een nette melding als er niets te laten zien is
.monospaced Vaste letterbreedte — handig voor getallen die netjes uitlijnen

10.7 Opdracht

Voeg aan de PrijsRij een tweede stijl toe:

  1. Als het huidig uur is: toon de prijs vet en vergroot de gekleurde stip naar 12×12.
  2. Als de prijs ontbreekt (nil): toon het streepje in een andere kleur en voeg de tekst “Geen data” toe naast het streepje.
  3. Voeg een header toe bovenaan de lijst met de naam van de databron en de laatste updatetijd.