15  H15: Debuggen en Instruments

Elke programmeur stuit vroeg of laat op een bug: iets doet niet wat je verwacht. Debuggen is de kunst van het opsporen van die fout. In dit hoofdstuk leer je de gereedschappen die Xcode biedt: breakpoints, de console, de Memory Graph Debugger en Instruments.

15.1 Wat gaan we bouwen?

We bouwen niets nieuws — we leren de gereedschappen die je gebruikt om de app die je al hebt gebouwd te begrijpen en te verbeteren. Aan het einde zet je logging in de EnergyDataManager zodat je altijd kunt zien wat de app aan het doen is.

Xcode met een gepauzeerde app en een breakpoint, de variableninspector is zichtbaar

De Xcode debugger met een breakpoint

15.2 Playground-voorbeeld

import Foundation

func deelDoor(_ getal: Int, door deler: Int) -> Int {
    // Stel een breakpoint in op de volgende regel in Xcode
    // door op het regelnummer te klikken
    let resultaat = getal / deler
    return resultaat
}

// Dit crasht als deler 0 is — dat is een bug
let antwoord = deelDoor(10, door: 2)
print("Antwoord: \(antwoord)")

// Uncomment de volgende regel om de crash te zien:
// let kapot = deelDoor(10, door: 0)

Klik op het regelnummer naast let resultaat om een breakpoint te zetten. Als de code die regel bereikt, pauzeert de uitvoering en kun je de waarden van getal en deler inspecteren.

15.3 Concept uitgelegd

15.3.1 Breakpoint: een rood stoplicht

Een breakpoint is een markering in je code die zegt: “stop hier, laat mij kijken.” Als de app de gemarkeerde regel bereikt, pauzeert hij — alsof je een rood stoplicht voor je code zet.

In Xcode klik je op het regelnummer aan de linkerkant van de code-editor om een breakpoint in of uit te schakelen. Een blauw pijltje verschijnt.

15.3.2 De debugconsole: je venster op de app

De debugconsole (onderin Xcode) toont alles wat je met print() afdrukt. Maar ook foutmeldingen en berichten van het systeem.

print("Laad gestart")
print("Aantal prijzen: \(prijzen.count)")

Gebruik print() vrijelijk terwijl je ontwikkelt — maar verwijder ze of vervang ze door Logger voor de uiteindelijke versie.

15.3.3 os.Logger: professionele logging

print() werkt prima tijdens ontwikkeling, maar voor een echte app gebruik je os.Logger. De voordelen:

  • Logs zijn zichtbaar in de macOS Console-app
  • Je kunt filteren op subsysteem en categorie
  • Berichten hebben niveaus: debug, info, warning, error
  • Logs worden automatisch verwijderd bij te hoge schijfruimtegebruik
import os

// Bron: EnergyClock/EnergyClockLogManager.swift
let logger = Logger(subsystem: "nl.jouwnaam.app", category: "netwerk")

logger.info("Prijzen geladen: \(prijzen.count, privacy: .public)")
logger.warning("API traag: \(responstijd, privacy: .public) ms")
logger.error("Verbinding mislukt: \(fout.localizedDescription, privacy: .public)")

De privacy: .public is nodig om de waarde zichtbaar te maken in logs — standaard worden waarden gemaskeerd om privacyredenen.

15.4 Code schrijven

Stap 1: de logger opzetten

Maak EnergyClockLogManager.swift:

import os

// Bron: EnergyClock/EnergyClockLogManager.swift
// Centrale logger voor de app
// Bekijk logs in Console.app — filter op: bnelissen.EnergyClock
enum AppLogger {
    static let netwerk  = Logger(subsystem: "bnelissen.EnergyClock", category: "netwerk")
    static let data     = Logger(subsystem: "bnelissen.EnergyClock", category: "data")
    static let algemeen = Logger(subsystem: "bnelissen.EnergyClock", category: "algemeen")
}

Stap 2: logging toevoegen aan de manager

Voeg log-aanroepen toe in EnergyDataManager:

func laadPrijzen() async {
    isLoading = true
    AppLogger.netwerk.info("Prijzen ophalen gestart van \(selectedSource.rawValue, privacy: .public)")

    do {
        energyPrices = try await api.laadHuidigePrijzen()
        AppLogger.netwerk.info("\(energyPrices.count, privacy: .public) prijzen ontvangen")
    } catch {
        AppLogger.netwerk.error("Laden mislukt: \(error.localizedDescription, privacy: .public)")
        energyPrices = maakVoorbeeldData()
    }
    isLoading = false
}

Stap 3: breakpoints gebruiken

  1. Zet een breakpoint op de regel energyPrices = try await api.laadHuidigePrijzen()
  2. Start de app (Cmd+R)
  3. Als de app de breakpoint bereikt, pauzeert hij
  4. Kijk in de Variables View (linksonderin) naar de huidige waarden
  5. Druk F6 om één stap verder te gaan (Step Over)
  6. Druk Cmd+Y om de app te laten verdergaan

Stap 4: een Exception Breakpoint toevoegen

Een Exception Breakpoint pauzeert automatisch bij elke fout, ook als je geen breakpoint op die specifieke regel hebt gezet:

  1. Open de Breakpoint Navigator (Cmd+8)
  2. Klik linksonderin op +
  3. Kies “Exception Breakpoint”

Nu stopt de app automatisch op de exacte regel waar een crash optreedt.

15.5 Instruments gebruiken

Instruments is een aparte tool die draait naast Xcode. Je opent hem via Xcode > Open Developer Tool > Instruments, of via Product > Profile (Cmd+I).

15.5.1 Time Profiler

Laat zien hoeveel tijd elke functie kost. Handig als de app traag aanvoelt.

  1. Start Instruments met Time Profiler
  2. Klik op de rode record-knop
  3. Gebruik de app een tijdje
  4. Stop de opname
  5. Kijk welke functies bovenaan staan — die kosten de meeste tijd

15.5.2 Memory Graph Debugger

Toont alle objecten in het geheugen en hoe ze naar elkaar verwijzen. Handig om geheugenlekken op te sporen.

In Xcode: terwijl de app draait, klik op het geheugendiagram-icoon onderin de debugbalk (het icoon met drie cirkels verbonden door lijnen).

Als je een object ziet dat al lang niet meer gebruikt wordt maar toch in het geheugen zit, heb je waarschijnlijk een retain cycle: twee objecten die naar elkaar verwijzen waardoor geen van beide vrijgegeven wordt.

NoteVerdieping: Automatic Reference Counting (ARC)

Swift gebruikt ARC (Automatic Reference Counting) om geheugen te beheren. Elke keer als je een verwijzing naar een object aanmaakt, gaat de teller omhoog. Als de teller nul bereikt, wordt het object uit het geheugen verwijderd.

Een retain cycle ontstaat als object A naar object B verwijst en object B ook naar object A verwijst. Beide tellers zijn dan altijd minimaal 1, dus geen van beide wordt ooit vrijgegeven — een geheugenlek.

De oplossing: gebruik weak of unowned voor één van de twee verwijzingen:

class A {
    var b: B?
}

class B {
    weak var a: A?  // zwakke verwijzing: verhoogt de teller niet
}

In moderne SwiftUI met @Observable zijn retain cycles minder een probleem, maar bij closures moet je soms oppassen: [weak self] in een closure voorkomt dat de closure een sterke verwijzing naar self vasthoudt.

15.6 Apple documentatie

Meer over de Xcode debugger:

developer.apple.com/documentation/xcode/diagnosing-and-resolving-bugs-in-your-running-app

Meer over os.Logger:

developer.apple.com/documentation/os/logger

Meer over Instruments:

developer.apple.com/documentation/xcode/improving-your-app-s-performance

Open de Logger-documentatie en zoek naar “privacy”. Lees waarom Apple standaard waarden maskeert in logs.

15.7 Samenvatting

Begrip Betekenis
Breakpoint Pauzeer de app op een specifieke regel
Exception Breakpoint Pauzeer automatisch bij elke crash
Debugconsole Toont print()-uitvoer en foutmeldingen
os.Logger Professionele logging met niveaus en filters
Console.app macOS-app om alle logs te bekijken
Instruments Tool voor prestatieanalyse
Time Profiler Meet hoeveel tijd functies kosten
Memory Graph Debugger Toont objecten en verwijzingen in het geheugen
Retain cycle Twee objecten die naar elkaar verwijzen — geheugenlek
ARC Automatic Reference Counting — Swift’s geheugenbeheer
weak Zwakke verwijzing die de teller niet verhoogt

15.8 Opdracht

  1. Voeg een breakpoint toe in laadPrijzen() vlak voor de API-aanroep. Start de app, laat hem de breakpoint bereiken en kijk in de Variables View welke waarden beschikbaar zijn.
  2. Open Console.app tijdens het draaien van de app. Filter op subsysteem bnelissen.EnergyClock. Wat zie je?
  3. Start de app met Instruments (Time Profiler). Klik een paar keer op de ververs-knop. Welke functie kost de meeste tijd?