6 H6: Animaties
Een statische klok is saai. In dit hoofdstuk geven we de wijzerplaat leven: een wijzer die soepel beweegt met de huidige tijd, en een laadanimatie waarbij de segmenten één voor één oplichten als de app start. Je leert hoe je withAnimation gebruikt en hoe je herhalende taken uitvoert met Task.
6.1 Wat gaan we bouwen?
Twee animaties:
- De tijdwijzer — beweegt elke seconde een klein stukje, vloeiend dankzij een lineaire animatie.
- De reveal-animatie — bij het laden verschijnen de 24 segmenten één voor één in een halve seconde.

6.2 Playground-voorbeeld
import SwiftUI
import PlaygroundSupport
struct AnimatieView: View {
@State private var hoek: Double = 0
var body: some View {
VStack(spacing: 20) {
// Een rechthoek die draait
Rectangle()
.fill(.blue)
.frame(width: 20, height: 80)
.rotationEffect(.degrees(hoek))
Button("Draai!") {
withAnimation(.linear(duration: 1)) {
hoek += 45
}
}
.buttonStyle(.borderedProminent)
}
.frame(width: 200, height: 200)
}
}
PlaygroundPage.current.setLiveView(AnimatieView())Klik op “Draai!” — de rechthoek roteert vloeiend 45 graden. Dat is withAnimation in één zin: verander een waarde, en SwiftUI animeert de overgang.
6.3 Concept uitgelegd
6.3.1 withAnimation: vertel SwiftUI dat het mooi mag zijn
Normaal verandert SwiftUI dingen direct: druk op knop, teller wordt 10. Met withAnimation zeg je: “doe het wel, maar neem de tijd”:
// Zonder animatie: direct
score = 10
// Met animatie: vloeiend in 0.5 seconde
withAnimation(.easeInOut(duration: 0.5)) {
score = 10
}SwiftUI berekent zelf alle tussenposities. Jij geeft alleen de begin- en eindwaarde op.
6.3.2 Task: werk op de achtergrond
Een wijzer die elke seconde beweegt, mag de rest van de app niet vertragen. Daarvoor gebruik je Task — een stuk werk dat op de achtergrond uitgevoerd wordt.
.task {
// Dit loopt op de achtergrond
while !Task.isCancelled {
// Wacht 1 seconde
try? await Task.sleep(for: .seconds(1))
// Doe iets
updateTijd()
}
}De while !Task.isCancelled-lus stopt automatisch als de view verdwijnt.
6.3.3 Hoeken berekenen
Om de wijzer op de goede positie te zetten, rekenen we de huidige tijd om naar graden:
- 24 uur = 360 graden
- 1 uur = 15 graden
- 12 uur 30 minuten = 12,5 × 15 = 187,5 graden
En omdat we 0 graden bovenaan willen, trekken we 90 graden af.
6.4 Code schrijven
Stap 1: de tijdwijzer
Voeg aan KlokView een @State toe voor de hoek, en een task om die bij te houden:
// Bron: EnergyClock/EnergyClockView.swift
struct KlokView: View {
let colorMapper: EnergyColorMapper
@State private var wijzerHoek: Double = 0
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)
tekenWijzer(context: context, middelpunt: middelpunt, straal: straal - 30, hoek: wijzerHoek)
}
.frame(width: 300, height: 300)
.task {
// Start direct
updateTijd()
// Herhaal elke seconde
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(1))
updateTijd()
}
}
}
private func updateTijd() {
let kalender = Calendar.current
let nu = Date.now
let uur = kalender.component(.hour, from: nu)
let minuut = kalender.component(.minute, from: nu)
let seconde = kalender.component(.second, from: nu)
// Bereken exacte hoek inclusief minuten en seconden
let uurMetMinuten = Double(uur) + Double(minuut) / 60.0 + Double(seconde) / 3600.0
let doelHoek = (uurMetMinuten * 360.0 / 24.0) - 90
withAnimation(.linear(duration: 1)) {
wijzerHoek = doelHoek
}
}
private func tekenWijzer(context: GraphicsContext, middelpunt: CGPoint, straal: CGFloat, hoek: Double) {
let boogsHoek = CGFloat(hoek * .pi / 180)
// Punt van de wijzer
let tip = CGPoint(
x: middelpunt.x + cos(boogsHoek) * straal,
y: middelpunt.y + sin(boogsHoek) * straal
)
// Contragewicht (tegenovergestelde kant)
let contra = CGPoint(
x: middelpunt.x - cos(boogsHoek) * 20,
y: middelpunt.y - sin(boogsHoek) * 20
)
var pad = Path()
pad.move(to: contra)
pad.addLine(to: tip)
context.stroke(pad, with: .color(.primary), lineWidth: 3)
}
}Stap 2: de reveal-animatie
Bij het laden willen we de segmenten één voor één laten verschijnen. Voeg een @State toe die bijhoudt hoeveel segmenten al zichtbaar zijn:
// Bron: EnergyClock/EnergyClockView.swift
@State private var zichtbareUren: Int = 24
// Voeg toe aan de body, na de bestaande .task:
.onAppear {
Task {
await laadAnimatie()
}
}En de animatiefunctie:
// Bron: EnergyClock/EnergyClockView.swift – loadAnimationClock()
private func laadAnimatie() async {
let duur: Double = 0.5 // totale duur in seconden
zichtbareUren = 0
let vertraging = duur / 24.0 // vertraging per uur
for uur in 1...24 {
try? await Task.sleep(for: .seconds(vertraging))
zichtbareUren = uur
}
}Pas tekenSegmenten aan om zichtbareUren te gebruiken:
for uur in 0..<min(zichtbareUren, 24) {
// ... rest van de code
}withAnimation accepteert een animatiecurve die beschrijft hoe snel de animatie gaat op verschillende momenten:
.linear— constante snelheid van begin tot eind.easeIn— begint langzaam, eindigt snel.easeOut— begint snel, eindigt langzaam.easeInOut— langzaam start, versnelt in het midden, vertraagt aan het einde.spring— overshoots het doel lichtjes, zoals een veer
Voor de wijzer gebruiken we .linear zodat hij in een constante snelheid beweegt — precies zoals een echte secondewijzer.
Achter de schermen interpoleert SwiftUI de waarden met behulp van deze easing functions, ook wel Bézier-curves genoemd. De .spring-animatie gebruikt een differentiaalvergelijking die de beweging van een massa aan een veer simuleert.
6.5 Apple documentatie
Meer over animaties in SwiftUI:
developer.apple.com/documentation/swiftui/animation
Meer over Task en async/await:
developer.apple.com/documentation/swift/task
Zoek in de SwiftUI animatiedocumentatie naar .linear, .easeIn en .spring. Probeer elk van de drie uit in het playground-voorbeeld bovenaan dit hoofdstuk.
6.6 Samenvatting
| Begrip | Betekenis |
|---|---|
withAnimation |
Animeert een verandering in @State |
.linear(duration:) |
Animatie met constante snelheid |
.easeInOut |
Animatie die versnelt en vertraagt |
Task |
Voert werk uit op de achtergrond |
Task.sleep(for:) |
Wacht een bepaalde tijd |
Task.isCancelled |
Controleert of de taak gestopt moet worden |
.task { } |
Start een Task als de view verschijnt; stopt automatisch als de view verdwijnt |
.onAppear { } |
Voert code uit als de view voor het eerst zichtbaar wordt |
| Radialen | Wiskundige hoekeenheid: 2π = 360 graden |
6.7 Opdracht
Pas de reveal-animatie aan:
- Maak de animatie sneller: gebruik
0.3seconden in plaats van0.5. - Voeg een klein geluid toe als de animatie klaar is (hint: kijk naar
AudioServicesPlaySystemSounduit hetAudioToolbox-framework — maar kijk eerst of je dit kunt vinden in de Apple documentatie). - Moeilijker: zorg dat de reveal-animatie opnieuw afspeelt als je op een knop drukt.