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.

Een medium widget met de klokvisualisatie en een kleurenlegenda

De EnergyClock widget op het macOS-bureaublad

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 laadt
  • getSnapshot — een momentopname voor de widgetgalerij
  • getTimeline — 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:

  1. Selecteer de app-target → Signing & Capabilities+ CapabilityApp Groups
  2. Klik op + en voeg toe: group.bnelissen.EnergyClock
  3. 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])
    }
}
NoteVerdieping: waarom widgets geen live-updates hebben

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:

  1. Toon het huidige uur met een gekleurde stip (gebruik EnergyColorMapper of een eenvoudige kleurberekening in de widget zelf).
  2. Voeg een label toe rechtsonder met de laatste updatetijd.
  3. Test de widget in de widgetgalerij: run de widget-target in Xcode en kijk of je hem kunt toevoegen aan je bureaublad.