7  H7: Netwerk en async/await

De klok draait, de kleuren kloppen — maar de prijzen zijn nog nep. Het wordt tijd om echte data op te halen van het internet. In dit hoofdstuk leer je hoe je een netwerkaanroep doet in Swift, hoe je JSON vertaalt naar jouw eigen structs, en hoe je async/await gebruikt zodat je app niet bevriest tijdens het wachten.

7.1 Wat gaan we bouwen?

De EnergyAPIClient: een struct die prijzen ophaalt van de easyEnergy API en ze teruggeeft als een lijst van EnergyPrice-objecten. Na dit hoofdstuk haalt de app echte stroomprijzen op.

De klok met gekleurde segmenten op basis van echte prijzen van vandaag

De app toont echte stroomprijzen opgehaald van het internet

7.2 Playground-voorbeeld

import Foundation

// Maak een eenvoudige struct die we willen decoderen
struct Artikel: Codable {
    let id: Int
    let title: String
    let completed: Bool
}

// Haal één nep-taak op van een test-API
func haalArtikelOp() async throws -> Artikel {
    let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(Artikel.self, from: data)
}

// Start de aanroep
Task {
    do {
        let artikel = try await haalArtikelOp()
        print("Titel: \(artikel.title)")
        print("Klaar: \(artikel.completed)")
    } catch {
        print("Fout: \(error)")
    }
}

Dit haalt een JSON-object op van het internet en zet het om naar een Swift-struct. Dat is in drie regels geregeld.

7.3 Concept uitgelegd

7.3.1 Async/await: wachten zonder te blokkeren

Stel je voor dat je een bibliotheekboek bestelt. Je geeft de bestelling door aan de balie, en daarna ga je gewoon zitten lezen terwijl de medewerker het boek gaat zoeken. Je wacht, maar je zit niet te staren naar de balie — je doet ondertussen iets nuttigs.

Zo werkt async/await. De app doet een aanroep naar het internet (await), en terwijl het netwerk bezig is, blijft de interface gewoon werken. Je kunt scrollen, klikken, dingen doen. Zodra het antwoord er is, gaat de code verder.

// Zonder async/await: de app bevriest tijdens het wachten
// (dit werkt niet meer in modern Swift)

// Met async/await: de app blijft reageren
let data = try await URLSession.shared.data(from: url)

7.3.2 JSON: data als tekst

Servers sturen data terug als JSON — een soort tekst die eruitziet als een woordenboek:

{
  "hour": 8,
  "price": 0.09,
  "date": "2026-03-22T08:00:00Z"
}

Swift kan dit automatisch vertalen naar een struct, mits je struct Codable is en de veldnamen overeenkomen.

7.3.3 throws en do/catch: omgaan met fouten

Een netwerkaanroep kan mislukken: geen internet, server down, ongeldige data. Functies die kunnen mislukken zijn gemarkeerd met throws. Je roept ze aan met try, en je vangt fouten op met do/catch:

do {
    let prijzen = try await haalPrijzenOp()
    // succes
} catch {
    print("Fout: \(error)")
    // toon een melding aan de gebruiker
}

7.4 Code schrijven

Stap 1: het API-model

Maak APIModels.swift. Hier staan structs die precies overeenkomen met wat de API teruggeeft:

import Foundation

// Bron: EnergyClock/APIModels.swift
// Decodeermodel voor de easyEnergy API
struct EasyEnergyItem: Codable {
    let Timestamp: Date
    let TariffReturn: Double
}

De veldnamen Timestamp en TariffReturn moeten exact overeenkomen met de JSON die de API stuurt.

Stap 2: de API-client

Maak EnergyAPIClient.swift:

import Foundation

// Bron: EnergyClock/EnergyAPIClient.swift
// Haalt energieprijzen op van de easyEnergy API
struct EnergyAPIClient {

    func laadHuidigePrijzen() async throws -> [EnergyPrice] {
        let kalender = Calendar.current
        let vandaag = kalender.startOfDay(for: Date())
        let overmorgen = kalender.date(byAdding: .day, value: 2, to: vandaag) ?? vandaag

        let isoStijl = Date.ISO8601FormatStyle()
            .year().month().day()
            .time(includingFractionalSeconds: true)
            .timeZone(separator: .colon)

        var components = URLComponents()
        components.scheme = "https"
        components.host = "mijn.easyenergy.com"
        components.path = "/nl/api/tariff/getapxtariffs"
        components.queryItems = [
            URLQueryItem(name: "startTimestamp", value: vandaag.formatted(isoStijl)),
            URLQueryItem(name: "endTimestamp",   value: overmorgen.formatted(isoStijl)),
        ]

        guard let url = components.url else {
            throw APIFout.ongedigeURL("easyEnergy URL kon niet worden gebouwd")
        }

        let (data, _) = try await URLSession.shared.data(from: url)

        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        let items = try decoder.decode([EasyEnergyItem].self, from: data)

        return converteerEasyEnergy(items)
    }

    // Zet easyEnergy-items om naar EnergyPrice-objecten
    private func converteerEasyEnergy(_ items: [EasyEnergyItem]) -> [EnergyPrice] {
        let kalender = Calendar.current
        return items.map { item in
            let startVanUur = kalender.date(
                from: kalender.dateComponents([.year, .month, .day, .hour], from: item.Timestamp)
            ) ?? item.Timestamp
            let uur = kalender.component(.hour, from: startVanUur)
            return EnergyPrice(hour: uur, price: item.TariffReturn, date: startVanUur)
        }.sorted { $0.date < $1.date }
    }
}

// Fouttypen voor API-aanroepen
// Bron: EnergyClock/EnergyAPIClient.swift
enum APIFout: Error, LocalizedError {
    case ongedigeURL(String)

    var errorDescription: String? {
        switch self {
        case .ongedigeURL(let url): return "Ongeldige URL: \(url)"
        }
    }
}

Stap 3: de client gebruiken in een view

In de ContentView kun je de client aanroepen via .task:

struct ContentView: View {
    @State private var prijzen: [EnergyPrice] = []
    @State private var fout: String?

    let client = EnergyAPIClient()

    var body: some View {
        VStack {
            if let fout {
                Text("Fout: \(fout)")
                    .foregroundStyle(.red)
            } else {
                Text("\(prijzen.count) prijzen geladen")
            }
        }
        .task {
            do {
                prijzen = try await client.laadHuidigePrijzen()
            } catch {
                fout = error.localizedDescription
            }
        }
    }
}
NoteVerdieping: het HTTP-protocol en TCP/IP

Als jouw app URLSession.shared.data(from: url) aanroept, gebeurt er veel onder de motorkap:

  1. DNS-opzoeking — de domeinnaam (mijn.easyenergy.com) wordt omgezet naar een IP-adres via het Domain Name System.
  2. TCP-verbinding — er wordt een betrouwbare verbinding opgezet met het TCP-protocol (Transmission Control Protocol).
  3. TLS-handshake — de verbinding wordt versleuteld met TLS zodat niemand kan meelezen.
  4. HTTP-verzoek — de app stuurt een GET-verzoek met headers en URL-parameters.
  5. HTTP-antwoord — de server stuurt JSON terug met een statuscodes (200 = OK, 404 = niet gevonden, 500 = serverfout).

URLSession regelt dit allemaal voor je. Het is een hoge-niveau abstractie bovenop de BSD socket API die al decennialang in Unix-systemen zit.

De async/await-interface van URLSession maakt gebruik van Swift’s structured concurrency: taken hebben een duidelijke levensduur, fouten propageren netjes omhoog, en annulering werkt automatisch als de view verdwijnt.

7.5 Apple documentatie

Alles over URLSession:

developer.apple.com/documentation/foundation/urlsession

Alles over Codable en JSON:

developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types

Meer over async/await:

docs.swift.org — Concurrency

Zoek in de URLSession-documentatie naar data(from:). Lees de “Discussion” sectie — die legt uit hoe annulering werkt als de view verdwijnt.

7.6 Samenvatting

Begrip Betekenis
async Markeert een functie die kan wachten
await Wacht op het resultaat van een async functie
throws Markeert een functie die een fout kan gooien
try Roept een throwing functie aan
do / catch Probeert code uit; vangt fouten op
URLSession De netwerk-laag van Apple
JSONDecoder Zet JSON-tekst om naar Swift-structs
Codable Protocol dat structs decodeerbaar maakt vanuit JSON
URLComponents Bouwt een URL op uit losse onderdelen

7.7 Opdracht

Breid de EnergyAPIClient uit:

  1. Voeg een functie toe laadEnergyZero() die prijzen ophaalt van api.energyzero.nl. De URL-structuur vind je in de broncode van de app (EnergyAPIClient.swift).
  2. Maak een EnergyZeroResponse en EnergyZeroPrice struct in APIModels.swift die overeenkomen met de JSON van EnergyZero.
  3. Test de nieuwe functie door hem aan te roepen vanuit een .task in een simpele view die het aantal ontvangen prijzen afdrukt.