5  H5: Kleuren en logica

De wijzerplaat staat. Nu moeten de segmenten een kleur krijgen op basis van de stroomprijs. Goedkope stroom is groen, dure stroom is rood. In dit hoofdstuk leer je hoe je keuzes maakt in Swift — met if, switch en enums — en hoe je die logica netjes verpakt in een aparte struct.

5.1 Wat gaan we bouwen?

De EnergyColorMapper: een struct die een prijs ontvangt en een kleur teruggeeft. Na dit hoofdstuk kleurt de wijzerplaat automatisch mee met de echte prijzen.

De wijzerplaat met segmenten in blauw, groen, geel, oranje en rood op basis van prijzen

Wijzerplaat met gekleurde segmenten van blauw tot rood

5.2 Playground-voorbeeld

import SwiftUI
import PlaygroundSupport

// Een eenvoudige kleurmapper
func kleurVoorPrijs(_ prijs: Double) -> Color {
    if prijs < 0 {
        return .blue    // negatieve prijs: stroom leveren loont
    } else if prijs < 0.10 {
        return .green   // goedkoop
    } else if prijs < 0.20 {
        return .yellow  // gemiddeld
    } else if prijs < 0.30 {
        return .orange  // duur
    } else {
        return .red     // erg duur
    }
}

// Test de functie
let testPrijzen: [Double] = [-0.05, 0.05, 0.15, 0.25, 0.40]
for prijs in testPrijzen {
    let kleur = kleurVoorPrijs(prijs)
    print("Prijs \(prijs): \(kleur)")
}

5.3 Concept uitgelegd

5.3.1 Enum: een keuzemenu

Een enum (spreek uit: “ie-num”) is een lijst met vaste keuzes. Je kent het van de versnellingspook: je kunt alleen kiezen uit P, R, N of D — niet uit “misschien” of “een beetje P”.

// Bron: EnergyClock/ColorMode.swift
enum KleurModus {
    case absoluut   // vaste drempelwaarden in euro's
    case relatief   // verhoudingsgewijs t.o.v. de dag
}

Je gebruikt een enum-waarde zo:

var huidigeKleurModus = KleurModus.absoluut

5.3.2 Switch: een meerkeuzetoets

Waar if één vraag stelt, bekijkt switch een waarde en vergelijkt die met meerdere mogelijkheden:

switch huidigeKleurModus {
case .absoluut:
    print("Vaste grenzen gebruiken")
case .relatief:
    print("Verhoudingsgewijs kleuren")
}

Swift dwingt je om alle gevallen te behandelen. Als je een case vergeet, geeft de compiler een foutmelding. Dat is handig: later als je een nieuwe case aan de enum toevoegt, wijst Swift je automatisch op alle plekken in de code die je moet aanpassen.

5.3.3 Drempelwaarden: wanneer wordt iets rood?

In de absoluut-modus werken we met drempelwaarden: vaste euro-bedragen die bepalen welke kleur een prijs krijgt.

prijs < 0        → blauw  (negatief: je krijgt geld!)
prijs < €0,10   → groen
prijs < €0,20   → geel
prijs < €0,30   → oranje
prijs >= €0,30  → rood

5.4 Code schrijven

Stap 1: de ColorMode enum

Maak een nieuw bestand ColorMode.swift:

import Foundation

// Bron: EnergyClock/ColorMode.swift
// Bepaalt hoe prijzen worden omgezet naar kleuren
enum ColorMode: String, CaseIterable, Identifiable {
    case absolute = "Absoluut"
    case relative = "Relatief"

    var id: String { rawValue }
}

CaseIterable zorgt dat Swift automatisch een lijst kan maken van alle gevallen. Identifiable maakt de enum bruikbaar in een SwiftUI ForEach.

Stap 2: de EnergyColorMapper struct

Maak een nieuw bestand EnergyColorMapper.swift:

import SwiftUI

// Bron: EnergyClock/EnergyColorMapper.swift
// Mapped prijzen naar kleuren op basis van de gekozen kleurmodus
struct EnergyColorMapper {
    let prices: [EnergyPrice]
    let mode: ColorMode
    let absoluteThresholds: AbsoluteThresholds

    // Vaste drempelwaarden in EUR/kWh
    struct AbsoluteThresholds {
        var greenMax: Double    // standaard 0.10
        var yellowMax: Double   // standaard 0.20
        var orangeMax: Double   // standaard 0.30
    }

    // Geef de kleur voor een specifiek uur
    func color(for hour: Int) -> Color {
        guard let priceData = prices.first(where: { $0.hour == hour }),
              let price = priceData.price else {
            return .gray  // geen data beschikbaar
        }
        return colorForPrice(price)
    }

    private func colorForPrice(_ price: Double) -> Color {
        if price < 0 {
            return .blue
        }
        switch mode {
        case .absolute:
            return absoluteColor(for: price)
        case .relative:
            return .gray  // relatieve modus volgt in H8
        }
    }

    private func absoluteColor(for price: Double) -> Color {
        if price < absoluteThresholds.greenMax {
            return .green
        } else if price < absoluteThresholds.yellowMax {
            return .yellow
        } else if price < absoluteThresholds.orangeMax {
            return .orange
        } else {
            return .red
        }
    }
}

Stap 3: de kleurmapper koppelen aan de wijzerplaat

Pas KlokView aan om de mapper te gebruiken:

struct KlokView: View {
    let colorMapper: EnergyColorMapper

    var body: some View {
        Canvas { context, size in
            let middelpunt = CGPoint(x: size.width / 2, y: size.height / 2)
            let straal = (min(size.width, size.height) / 2) - 20
            tekenSegmenten(context: context, middelpunt: middelpunt, straal: straal)
        }
        .frame(width: 300, height: 300)
    }

    private func tekenSegmenten(context: GraphicsContext, middelpunt: CGPoint, straal: CGFloat) {
        for uur in 0..<24 {
            let beginGraden = Double(uur) * 360.0 / 24.0 - 90
            let eindGraden  = Double(uur + 1) * 360.0 / 24.0 - 90 - 2

            var pad = Path()
            pad.addArc(
                center: middelpunt,
                radius: straal,
                startAngle: .degrees(beginGraden),
                endAngle: .degrees(eindGraden),
                clockwise: false
            )

            // Kleur komt nu uit de mapper
            let kleur = colorMapper.color(for: uur)
            context.stroke(pad, with: .color(kleur), lineWidth: 16)
        }
    }
}
NoteVerdieping: enums met geassocieerde waarden

In dit hoofdstuk gebruiken we een eenvoudige enum met twee cases. Swift-enums kunnen echter ook geassocieerde waarden hebben: extra informatie die per case verschilt.

enum Prijs {
    case bekend(Double)    // een bedrag
    case onbekend          // geen data
    case negatief(Double)  // je krijgt geld terug
}

Dit maakt enums veel expressiever dan de simpele lijsten uit andere talen. Samen met switch en pattern matching vormen ze een krachtig systeem om alle mogelijke toestanden van een waarde te beschrijven en af te handelen — zonder dat je ooit een geval kunt missen.

Dit concept, waarbij je de shape van data beschrijft met types in plaats van met conditionals, heet algebraic data types in de taaltheorie.

5.5 Apple documentatie

Meer over enums in Swift:

docs.swift.org — Enumerations

Meer over switch en pattern matching:

docs.swift.org — Control Flow

Zoek in de documentatie naar het hoofdstuk “Switch”. Let op het stuk over “Exhaustiveness” — dat legt uit waarom Swift je dwingt alle gevallen te behandelen.

5.6 Samenvatting

Begrip Betekenis
enum Een type met een vaste set keuzes
switch Vergelijkt een waarde met meerdere gevallen
CaseIterable Protocol: Swift maakt automatisch een lijst van alle cases
Identifiable Protocol: elke instantie heeft een unieke id
Drempelwaarde Een grenswaarde die bepaalt welke categorie iets valt
guard let Controleert een optional; springt uit de functie als hij leeg is

5.7 Opdracht

Pas de EnergyColorMapper aan:

  1. Voeg een vierde drempelwaarde toe: alles onder 0.05 wordt cyan (superveel goedkoop). Test dit met een prijs van 0.03.
  2. Maak een kleine testfunctie in een Playground die voor de prijzen [-0.1, 0.03, 0.08, 0.15, 0.25, 0.35] de kleur afdrukt met print().