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.

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
- Zet een breakpoint op de regel
energyPrices = try await api.laadHuidigePrijzen() - Start de app (
Cmd+R) - Als de app de breakpoint bereikt, pauzeert hij
- Kijk in de Variables View (linksonderin) naar de huidige waarden
- Druk
F6om één stap verder te gaan (Step Over) - Druk
Cmd+Yom 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:
- Open de Breakpoint Navigator (
Cmd+8) - Klik linksonderin op
+ - 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.
- Start Instruments met Time Profiler
- Klik op de rode record-knop
- Gebruik de app een tijdje
- Stop de opname
- 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.
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
- 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. - Open Console.app tijdens het draaien van de app. Filter op subsysteem
bnelissen.EnergyClock. Wat zie je? - Start de app met Instruments (Time Profiler). Klik een paar keer op de ververs-knop. Welke functie kost de meeste tijd?