13 H13: Widget
Een widget is een mini-versie van je app die op het bureaublad of in het berichtencentrum staat. Hij toont altijd de meest recente informatie zonder dat je de app hoeft te openen. In dit hoofdstuk voeg je een widget toe aan de EnergyClock die de klok in een klein formaat toont.
13.1 Wat gaan we bouwen?
Een macOS-widget van het formaat “medium” die de 24-uurs klok toont naast een kleurenlegenda. De widget gebruikt dezelfde data als de app via een gedeelde App Group.

13.2 Playground-voorbeeld
Widgets kunnen niet in Playgrounds getest worden — ze hebben WidgetKit nodig dat alleen in een app werkt. Maar je kunt de weergave wel oefenen in een gewone SwiftUI-view:
import SwiftUI
import PlaygroundSupport
// Simuleer hoe een widget eruit ziet
struct WidgetSimulatie: View {
var body: some View {
ZStack {
Color.black
VStack(alignment: .leading) {
Text("EnergyClock")
.font(.caption)
.foregroundStyle(.secondary)
Text("Vandaag")
.font(.headline)
.foregroundStyle(.white)
Spacer()
HStack {
Circle().fill(.green).frame(width: 12, height: 12)
Text("Goedkoop")
.font(.caption2)
.foregroundStyle(.white)
}
}
.padding()
}
.frame(width: 329, height: 155)
.clipShape(.rect(cornerRadius: 20))
}
}
PlaygroundPage.current.setLiveView(WidgetSimulatie())13.3 Concept uitgelegd
13.3.1 Hoe een widget werkt
Een widget draait niet de hele tijd. In plaats daarvan vraagt WidgetKit eens per dag (of na een bepaald tijdstip): “geef mij een tijdlijn van momenten waarop ik de widget moet verversen.” Jij levert dan een lijst van TimelineEntry-objecten, elk met een datum en de data voor dat moment.
App slaat data op → UserDefaults (App Group)
↓
Widget leest data op → TimelineProvider
↓
WidgetKit toont entry → WidgetView
13.3.2 TimelineProvider: de planner
Een TimelineProvider heeft drie functies:
placeholder— een nep-versie terwijl de widget laadtgetSnapshot— een momentopname voor de widgetgalerijgetTimeline— de echte tijdlijn van entries
func getTimeline(in context: Context, completion: @escaping (Timeline<MijnEntry>) -> Void) {
let entry = MijnEntry(date: .now, tekst: "Hallo")
// Ververs morgenochtend om 00:05
let verversOm = Calendar.current.date(byAdding: .minute, value: 5, to: Calendar.current.startOfDay(for: .now.addingTimeInterval(86400))) ?? .now
let timeline = Timeline(entries: [entry], policy: .after(verversOm))
completion(timeline)
}13.3.3 App Group: data delen
De widget is een apart proces — het heeft geen toegang tot de geheugenruimte van de app. Data delen gaat via een App Group: gedeelde UserDefaults met dezelfde identifier in zowel de app als de widget.
// In de app (EnergyPersistence.swift):
UserDefaults(suiteName: "group.bnelissen.EnergyClock")
// In de widget (EnergyClockWidget.swift):
UserDefaults(suiteName: "group.bnelissen.EnergyClock")13.4 Code schrijven
Stap 1: widget extension toevoegen
In Xcode: File > New > Target > Widget Extension. Geef hem de naam EnergyClockWidget. Vink Include Configuration Intent uit.
Xcode maakt automatisch een nieuwe target aan met een apart mapje.
Stap 2: App Group instellen
Voeg een App Group toe aan zowel de app als de widget:
- Selecteer de app-target → Signing & Capabilities → + Capability → App Groups
- Klik op
+en voeg toe:group.bnelissen.EnergyClock - Herhaal voor de widget-target
Stap 3: de data-entry
import WidgetKit
import SwiftUI
// Bron: EnergyClock/EnergyClockWidget/EnergyClockWidget.swift
// Data die de widget ontvangt per tijdstip
struct EnergyClockEntry: TimelineEntry {
let date: Date
let prices: [WidgetEnergyPrice]
}
// Vereenvoudigd prijsmodel voor de widget
struct WidgetEnergyPrice: Codable {
let hour: Int
let price: Double?
}Stap 4: de TimelineProvider
// Bron: EnergyClock/EnergyClockWidget/EnergyClockWidget.swift
struct EnergyClockProvider: TimelineProvider {
private let appGroupID = "group.bnelissen.EnergyClock"
func placeholder(in context: Context) -> EnergyClockEntry {
EnergyClockEntry(date: .now, prices: voorbeeldPrijzen())
}
func getSnapshot(in context: Context, completion: @escaping (EnergyClockEntry) -> Void) {
completion(laadEntry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<EnergyClockEntry>) -> Void) {
let entry = laadEntry()
// Ververs morgen om 00:05
let kalender = Calendar.current
let morgen = kalender.startOfDay(for: .now.addingTimeInterval(86400))
let verversOm = kalender.date(byAdding: .minute, value: 5, to: morgen) ?? morgen
completion(Timeline(entries: [entry], policy: .after(verversOm)))
}
private func laadEntry() -> EnergyClockEntry {
let defaults = UserDefaults(suiteName: appGroupID) ?? .standard
var prices = voorbeeldPrijzen()
if let data = defaults.data(forKey: "savedEnergyPrices"),
let decoded = try? JSONDecoder().decode([StoredPrice].self, from: data) {
let kalender = Calendar.current
let vandaag = kalender.startOfDay(for: .now)
let morgen = kalender.date(byAdding: .day, value: 1, to: vandaag) ?? vandaag
let vandaagPrijzen = decoded.filter { $0.date >= vandaag && $0.date < morgen }
if !vandaagPrijzen.isEmpty {
prices = vandaagPrijzen.map { WidgetEnergyPrice(hour: $0.hour, price: $0.price) }
}
}
return EnergyClockEntry(date: .now, prices: prices)
}
private func voorbeeldPrijzen() -> [WidgetEnergyPrice] {
let waarden: [Double] = [0.08, 0.07, 0.06, 0.07, 0.09, 0.12,
0.16, 0.22, 0.26, 0.24, 0.21, 0.19,
0.18, 0.17, 0.18, 0.20, 0.23, 0.28,
0.31, 0.27, 0.22, 0.18, 0.14, 0.10]
return waarden.enumerated().map { WidgetEnergyPrice(hour: $0.offset, price: $0.element) }
}
}
private struct StoredPrice: Codable {
let hour: Int
let price: Double?
let date: Date
}Stap 5: de widget-view en definitie
// Bron: EnergyClock/EnergyClockWidget/EnergyClockWidget.swift
struct EnergyClockWidgetView: View {
let entry: EnergyClockEntry
var body: some View {
Text("Widget: \(entry.prices.count) prijzen")
.foregroundStyle(.white)
}
}
struct EnergyClockWidget: Widget {
let kind = "EnergyClockWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: EnergyClockProvider()) { entry in
EnergyClockWidgetView(entry: entry)
.containerBackground(.black, for: .widget)
}
.configurationDisplayName("EnergyClock")
.description("Toont de energieprijzen van vandaag als 24-uurs klok.")
.supportedFamilies([.systemMedium])
}
}Widgets zijn bewust ontworpen zonder live-updates. Ze volgen een snapshot-model: je levert een tijdlijn van statische entries, en WidgetKit kiest welke entry op welk moment getoond wordt.
Dit is een bewuste batterij- en prestatiekeuze van Apple. Een widget die elke seconde ophaalt en rendert zou even zwaar zijn als de app zelf. Door de tijdlijn vooraf te berekenen, kan iOS/macOS widgets efficiënt tonen zonder de processor wakker te houden.
Voor de klok betekent dit dat de weergave statisch is totdat de volgende entry in de tijdlijn wordt getoond. De wijzer beweegt dus niet in de widget — dat onderscheidt de widget van de echte app. Je kunt dit omzeilen met een AccessoryWidget en de .timer-modifier, maar dat is buiten het bereik van dit boek.
13.5 Apple documentatie
Alles over WidgetKit:
developer.apple.com/documentation/widgetkit
Alles over App Groups:
developer.apple.com/documentation/xcode/configuring-app-groups
Zoek in de WidgetKit-documentatie naar “Creating a Widget Extension”. Dat is de officiële Apple-tutorial voor het toevoegen van je eerste widget.
13.6 Samenvatting
| Begrip | Betekenis |
|---|---|
| Widget | Een mini-app op het bureaublad of in het berichtencentrum |
| WidgetKit | Het Apple-framework voor widgets |
TimelineEntry |
Een moment in de tijdlijn met bijbehorende data |
TimelineProvider |
Levert de tijdlijn van entries aan WidgetKit |
Timeline |
Een reeks entries met een ververspolicy |
| App Group | Gedeelde opslag tussen app en widget |
containerBackground |
Achtergrond van de widget |
.systemMedium |
Het medium widgetformaat (329×155 pt) |
13.7 Opdracht
Verbeter de widget-view:
- Toon het huidige uur met een gekleurde stip (gebruik
EnergyColorMapperof een eenvoudige kleurberekening in de widget zelf). - Voeg een label toe rechtsonder met de laatste updatetijd.
- Test de widget in de widgetgalerij: run de widget-target in Xcode en kijk of je hem kunt toevoegen aan je bureaublad.