WidgetKit iOS 17+: Interactieve Widgets met App Intents
Volledige gids voor het maken van interactieve iOS-widgets met WidgetKit en App Intents. Knoppen, toggles, animaties en best practices voor iOS 17+ in 2026.

iOS 17 heeft WidgetKit gerevolutioneerd door native interactiviteit te introduceren. Widgets zijn niet langer statische weergaven: ze kunnen nu rechtstreeks vanaf het beginscherm reageren op gebruikersacties zonder de app te openen. Deze belangrijke evolutie steunt op het App Intents framework en biedt een vloeiende, moderne gebruikerservaring.
Dit artikel behandelt het volledige proces van het maken van interactieve iOS 17+ widgets, van projectconfiguratie tot geavanceerde patronen met animaties en statusbeheer.
Architectuur van Interactieve Widgets
De interactiviteit van iOS 17+ widgets werkt via het App Intents framework. In tegenstelling tot traditionele deeplinks die de app zouden openen, maken App Intents het mogelijk om code rechtstreeks vanuit de widget uit te voeren en daarna automatisch de weergave te vernieuwen met de nieuwe gegevens.
import WidgetKit
import SwiftUI
import AppIntents
// De architectuur is gebaseerd op drie hoofdcomponenten:
// 1. Widget Timeline Provider - levert de gegevens
// 2. Widget View - toont de interface met Button/Toggle
// 3. App Intent - voert de actie uit bij aanraking
struct TaskWidget: Widget {
// Unieke widget-identificatie
let kind: String = "TaskWidget"
var body: some WidgetConfiguration {
// StaticConfiguration voor widgets zonder parameters
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
TaskWidgetView(entry: entry)
// Verplicht voor App Intents
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Taken")
.description("Beheer uw taken vanaf het beginscherm.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}De widget declareert zijn configuratie en specificeert de provider die de gegevens zal leveren. Het .containerBackground attribuut is verplicht sinds iOS 17 voor interactieve widgets.
Aanmaken van de Timeline Provider
De Timeline Provider bepaalt wanneer en hoe de widget wordt vernieuwd. Voor interactieve widgets moet hij ook reageren op wijzigingen veroorzaakt door App Intents.
import WidgetKit
import SwiftUI
// Entry die de widgetstatus op een bepaald moment vertegenwoordigt
struct TaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Laadstatus voor visuele feedback
var isLoading: Bool = false
}
// Gegevensmodel gedeeld tussen app en widget
struct Task: Identifiable, Codable {
let id: UUID
var title: String
var isCompleted: Bool
var priority: Priority
enum Priority: String, Codable {
case low, medium, high
}
}
struct TaskTimelineProvider: TimelineProvider {
// Placeholder weergegeven tijdens initieel laden
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(
date: Date(),
tasks: [
Task(id: UUID(), title: "Voorbeeldtaak", isCompleted: false, priority: .medium)
]
)
}
// Snapshot voor de widgetgalerij
func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
let entry = TaskEntry(
date: Date(),
tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 }
)
completion(entry)
}
// Volledige timeline met vernieuwingsbeleid
func getTimeline(in context: Context, completion: @escaping (Timeline<TaskEntry>) -> Void) {
let tasks = TaskDataManager.shared.fetchTasks()
let entry = TaskEntry(date: Date(), tasks: Array(tasks.prefix(3)))
// Vernieuwing over 15 minuten of na een gebruikersactie
let nextUpdate = Calendar.current.date(
byAdding: .minute,
value: 15,
to: Date()
) ?? Date()
let timeline = Timeline(
entries: [entry],
policy: .after(nextUpdate)
)
completion(timeline)
}
}De provider gebruikt een gedeelde TaskDataManager om toegang te krijgen tot de gegevens. Deze aanpak garandeert synchronisatie tussen de hoofdapplicatie en de widget.
Om gegevens te delen tussen app en widget moet een App Group worden geconfigureerd in de project capabilities. UserDefaults of bestanden moeten deze gedeelde groep gebruiken.
De Manager voor Gedeelde Gegevens
Het delen van gegevens tussen de applicatie en de widget vereist een gemeenschappelijke container die toegankelijk is via App Group.
import Foundation
final class TaskDataManager {
// Singleton voor globale toegang
static let shared = TaskDataManager()
// App Group identifier geconfigureerd in Xcode
private let appGroupID = "group.com.example.taskapp"
// UserDefaults gedeeld tussen app en widget
private var sharedDefaults: UserDefaults? {
UserDefaults(suiteName: appGroupID)
}
private let tasksKey = "tasks"
private init() {}
// Haalt taken op uit gedeelde opslag
func fetchTasks() -> [Task] {
guard let data = sharedDefaults?.data(forKey: tasksKey),
let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
return []
}
return tasks
}
// Opslaan met widgetmelding
func saveTasks(_ tasks: [Task]) {
guard let data = try? JSONEncoder().encode(tasks) else { return }
sharedDefaults?.set(data, forKey: tasksKey)
}
// Werkt een specifieke taak bij
func updateTask(_ task: Task) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index] = task
saveTasks(tasks)
}
}
// Wisselt voltooiingsstatus om
func toggleTaskCompletion(taskID: UUID) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == taskID }) {
tasks[index].isCompleted.toggle()
saveTasks(tasks)
}
}
}Deze manager kapselt alle persistentielogica in en zal worden gebruikt door zowel de applicatie als de App Intents van de widget.
Aanmaken van het App Intent voor Interactiviteit
Het App Intent definieert de actie die wordt uitgevoerd wanneer de gebruiker met de widget interageert. iOS voert deze actie op de achtergrond uit en vernieuwt vervolgens automatisch de widget.
import AppIntents
import WidgetKit
// Intent om de taakstatus om te wisselen
struct ToggleTaskIntent: AppIntent {
// Titel weergegeven in Siri-snelkoppelingen
static var title: LocalizedStringResource = "Taakstatus omwisselen"
// Beschrijving voor toegankelijkheid
static var description = IntentDescription("Markeert een taak als voltooid of niet voltooid.")
// Parameter: ID van de te wijzigen taak
@Parameter(title: "Taak-ID")
var taskID: String
// Vereiste initializer voor AppIntent
init() {}
// Initializer met parameter voor aanmaak vanuit de view
init(taskID: UUID) {
self.taskID = taskID.uuidString
}
// Uitvoering van de actie
func perform() async throws -> some IntentResult {
// Conversie van string-ID naar UUID
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
// Update van de taak
TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
// Vraagt widget-vernieuwing aan
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}De oproep naar WidgetCenter.shared.reloadTimelines veroorzaakt een onmiddellijke widget-vernieuwing na de actie en garandeert directe visuele feedback.
Klaar om je iOS gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Widget-View met Interactieve Knoppen
De widget-view gebruikt het standaard SwiftUI Button component met de intent als actie. iOS 17+ onderschept deze interacties automatisch om het App Intent uit te voeren.
import SwiftUI
import WidgetKit
struct TaskWidgetView: View {
let entry: TaskEntry
// Aanpassing aan widgetgrootte
@Environment(\.widgetFamily) var family
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Header met titel en teller
headerView
// Takenlijst met interactieve knoppen
ForEach(entry.tasks.prefix(tasksLimit)) { task in
TaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
// Aantal taken volgens grootte
private var tasksLimit: Int {
switch family {
case .systemSmall: return 2
case .systemMedium: return 3
default: return 4
}
}
private var headerView: some View {
HStack {
Text("Taken")
.font(.headline)
.fontWeight(.bold)
Spacer()
// Badge met aantal resterende taken
let remaining = entry.tasks.filter { !$0.isCompleted }.count
Text("\(remaining)")
.font(.caption.bold())
.foregroundStyle(.white)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(remaining > 0 ? Color.orange : Color.green)
.clipShape(Capsule())
}
}
}
struct TaskRowView: View {
let task: Task
var body: some View {
// Knop met App Intent als actie
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Voltooiingsindicator
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Taaktitel
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
.lineLimit(1)
Spacer()
// Prioriteitsindicator
priorityIndicator
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(Color(.systemBackground).opacity(0.5))
.cornerRadius(8)
}
.buttonStyle(.plain)
}
@ViewBuilder
private var priorityIndicator: some View {
switch task.priority {
case .high:
Image(systemName: "exclamationmark.circle.fill")
.foregroundStyle(.red)
case .medium:
Image(systemName: "minus.circle.fill")
.foregroundStyle(.orange)
case .low:
EmptyView()
}
}
}De Button(intent:) syntax verbindt de knop rechtstreeks met het App Intent. Bij aanraking voert iOS perform() uit en vernieuwt vervolgens automatisch de widget.
Interactieve Toggle voor Widgets
Voor aan/uit-acties biedt het Toggle component een alternatief voor de knop met een native iOS-stijl.
import SwiftUI
import AppIntents
// Specifiek intent voor Toggle met expliciete status
struct SetTaskCompletionIntent: AppIntent {
static var title: LocalizedStringResource = "Taakstatus instellen"
@Parameter(title: "Taak-ID")
var taskID: String
// Doelstatus: true = voltooid, false = niet voltooid
@Parameter(title: "Voltooid")
var isCompleted: Bool
init() {}
init(taskID: UUID, isCompleted: Bool) {
self.taskID = taskID.uuidString
self.isCompleted = isCompleted
}
func perform() async throws -> some IntentResult {
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
var tasks = TaskDataManager.shared.fetchTasks()
if let index = tasks.firstIndex(where: { $0.id == uuid }) {
// Stelt status expliciet in (geen toggle)
tasks[index].isCompleted = isCompleted
TaskDataManager.shared.saveTasks(tasks)
}
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}
struct TaskToggleRowView: View {
let task: Task
var body: some View {
HStack {
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
Spacer()
// Interactieve toggle met intent
Toggle(
isOn: task.isCompleted,
intent: SetTaskCompletionIntent(
taskID: task.id,
isCompleted: !task.isCompleted
)
)
.toggleStyle(.switch)
.labelsHidden()
}
.padding(.vertical, 4)
}
}Toggle biedt een intuïtievere interactie voor binaire statussen en integreert natuurlijk in iOS-ontwerpen.
Widgets kunnen geen alerts, sheets of navigatie weergeven. Alle acties moeten autonoom zijn en de zichtbare status direct bijwerken.
Vernieuwingsanimaties en Overgangen
iOS 17+ maakt het mogelijk om overgangen te animeren tijdens widget-vernieuwing na een actie. De .contentTransition modifier regelt deze animaties.
import SwiftUI
import WidgetKit
struct AnimatedTaskRowView: View {
let task: Task
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Icoon met overgangsanimatie
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Icoonanimatie bij wijziging
.contentTransition(.symbolEffect(.replace))
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
// Tekstanimatie
.contentTransition(.opacity)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear)
)
// Achtergrondanimatie
.animation(.easeInOut(duration: 0.3), value: task.isCompleted)
}
.buttonStyle(.plain)
}
}
// Widget met geanimeerde invalidatie
struct AnimatedTaskWidget: Widget {
let kind: String = "AnimatedTaskWidget"
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
AnimatedTaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Geanimeerde Taken")
.description("Widgets met vloeiende animaties.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
// Activering van inhoudsanimaties
.contentMarginsDisabled()
}
}
struct AnimatedTaskWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
headerView
ForEach(entry.tasks) { task in
AnimatedTaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
private var headerView: some View {
HStack {
Text("Taken")
.font(.headline.bold())
Spacer()
let completed = entry.tasks.filter(\.isCompleted).count
let total = entry.tasks.count
// Geanimeerde voortgang
Text("\(completed)/\(total)")
.font(.caption.bold())
.foregroundStyle(.secondary)
.contentTransition(.numericText())
}
}
}De animaties .symbolEffect(.replace) en .numericText() creëren vloeiende overgangen tussen statussen en verbeteren de gebruikerservaring aanzienlijk.
Configureerbare Widget met AppIntentConfiguration
Voor door gebruikers aanpasbare widgets (filters, categorieën) vervangt AppIntentConfiguration de StaticConfiguration.
import WidgetKit
import SwiftUI
import AppIntents
// Configuratie blootgesteld aan de gebruiker
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Taakconfiguratie"
static var description = IntentDescription("Pas de taakweergave aan.")
// Filter op prioriteit
@Parameter(title: "Prioriteit", default: .all)
var priorityFilter: PriorityFilter
// Voltooide taken weergeven
@Parameter(title: "Voltooide weergeven", default: true)
var showCompleted: Bool
// Maximaal aantal taken
@Parameter(title: "Aantal taken", default: 3)
var maxTasks: Int
}
// Enum voor prioriteitsfilter
enum PriorityFilter: String, AppEnum {
case all
case high
case medium
case low
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Prioriteit"
static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
.all: "Alle",
.high: "Hoog",
.medium: "Gemiddeld",
.low: "Laag"
]
}
// Provider aangepast aan de configuratie
struct ConfigurableTaskProvider: AppIntentTimelineProvider {
typealias Entry = TaskEntry
typealias Intent = TaskWidgetConfigurationIntent
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(date: Date(), tasks: [])
}
func snapshot(for configuration: TaskWidgetConfigurationIntent, in context: Context) async -> TaskEntry {
let tasks = filteredTasks(for: configuration)
return TaskEntry(date: Date(), tasks: tasks)
}
func timeline(for configuration: TaskWidgetConfigurationIntent, in context: Context) async -> Timeline<TaskEntry> {
let tasks = filteredTasks(for: configuration)
let entry = TaskEntry(date: Date(), tasks: tasks)
let nextUpdate = Date().addingTimeInterval(15 * 60)
return Timeline(entries: [entry], policy: .after(nextUpdate))
}
// Past configuratiefilters toe
private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
var tasks = TaskDataManager.shared.fetchTasks()
// Filtering op prioriteit
if config.priorityFilter != .all {
let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
tasks = tasks.filter { $0.priority == priority }
}
// Filtering van voltooide indien nodig
if !config.showCompleted {
tasks = tasks.filter { !$0.isCompleted }
}
// Beperking van het aantal
return Array(tasks.prefix(config.maxTasks))
}
}
// Widget met gebruikersconfiguratie
struct ConfigurableTaskWidget: Widget {
let kind: String = "ConfigurableTaskWidget"
var body: some WidgetConfiguration {
// AppIntentConfiguration voor configureerbare widgets
AppIntentConfiguration(
kind: kind,
intent: TaskWidgetConfigurationIntent.self,
provider: ConfigurableTaskProvider()
) { entry in
TaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Aangepaste Taken")
.description("Filter en pas uw taken aan.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}De gebruiker kan de widget nu configureren door lang in te drukken en zo een gepersonaliseerde ervaring krijgen zonder extra code in de applicatie.
Foutafhandeling en Laadstatussen
Een goede UX vereist het afhandelen van foutgevallen en tussentoestanden tijdens interacties.
import AppIntents
import WidgetKit
struct ToggleTaskWithFeedbackIntent: AppIntent {
static var title: LocalizedStringResource = "Taak omwisselen met feedback"
@Parameter(title: "Taak-ID")
var taskID: String
init() {}
init(taskID: UUID) {
self.taskID = taskID.uuidString
}
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
guard let uuid = UUID(uuidString: taskID) else {
// Stille fout retourneren
return .result(value: false)
}
// Simuleert async operatie (bijv. server-sync)
do {
try await Task.sleep(for: .milliseconds(100))
TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result(value: true)
} catch {
// Fout: widget niet bijwerken
return .result(value: false)
}
}
}
// View met laadstatus
struct TaskRowWithLoadingView: View {
let task: Task
@State private var isLoading = false
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Conditionele indicator
Group {
if isLoading {
ProgressView()
.scaleEffect(0.8)
} else {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(task.isCompleted ? .green : .secondary)
}
}
.frame(width: 24, height: 24)
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
Spacer()
}
.padding(.vertical, 6)
.padding(.horizontal, 10)
.background(Color(.systemBackground).opacity(0.5))
.cornerRadius(8)
}
.buttonStyle(.plain)
.disabled(isLoading)
.opacity(isLoading ? 0.6 : 1.0)
}
}Onmiddellijke visuele feedback (verminderde opaciteit, laadindicator) informeert de gebruiker dat zijn actie is geregistreerd.
Best Practices en Optimalisaties
Verschillende patronen garanderen prestatieve en betrouwbare interactieve widgets.
import WidgetKit
import SwiftUI
// 1. Cache altijd ongeldig maken na wijziging
final class WidgetRefreshManager {
static func refreshAllWidgets() {
// Vernieuwing van alle app-widgets
WidgetCenter.shared.reloadAllTimelines()
}
static func refreshWidget(kind: String) {
// Vernieuwing van een specifieke widget
WidgetCenter.shared.reloadTimelines(ofKind: kind)
}
// Oproep vanuit de app na gegevenswijziging
static func notifyDataChanged() {
Task { @MainActor in
refreshAllWidgets()
}
}
}
// 2. Beperk view-complexiteit
struct OptimizedWidgetView: View {
let entry: TaskEntry
var body: some View {
// Eenvoudige views zonder GeometryReader prefereren
VStack(alignment: .leading, spacing: 8) {
ForEach(entry.tasks.prefix(3)) { task in
// Lichtgewicht componenten
minimalTaskRow(task)
}
}
.padding()
}
// Minimale view voor prestaties
@ViewBuilder
private func minimalTaskRow(_ task: Task) -> some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
Text(task.title)
.lineLimit(1)
}
}
.buttonStyle(.plain)
}
}
// 3. Gebruik @AppStorage voor eenvoudige statussen
struct QuickSettingsWidgetView: View {
// Directe toegang tot gedeelde UserDefaults
@AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
private var showCompleted = true
var body: some View {
// Status blijft behouden tussen vernieuwingen
Text(showCompleted ? "Alle weergeven" : "Voltooide verbergen")
}
}
// 4. Pre-bereken gegevens in de provider
struct OptimizedTaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Voorberekende gegevens
let completedCount: Int
let pendingCount: Int
let highPriorityCount: Int
init(date: Date, tasks: [Task]) {
self.date = date
self.tasks = tasks
// Berekeningen één keer uitgevoerd
self.completedCount = tasks.filter(\.isCompleted).count
self.pendingCount = tasks.filter { !$0.isCompleted }.count
self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count
}
}Deze optimalisaties zorgen voor responsieve widgets die de batterij niet overmatig verbruiken.
Gebruik het widget-schema in Xcode voor debugging. Het canvas-voorbeeld maakt het mogelijk om verschillende groottes en statussen te testen zonder installatie op het apparaat.
Conclusie
WidgetKit iOS 17+ met App Intents transformeert widgets in echte interactieve uitbreidingen van iOS-applicaties. Deze declaratieve architectuur vereenvoudigt de ontwikkeling aanzienlijk en biedt een native, vloeiende gebruikerservaring.
Checklist Interactieve iOS 17+ Widget
- ✅ App Group configureren voor gegevensuitwisseling
- ✅ Timeline Provider aanmaken met geschikte vernieuwing
- ✅ App Intents implementeren voor elke actie
- ✅
Button(intent:)ofToggle(intent:)gebruiken voor interactiviteit - ✅
WidgetCenter.shared.reloadTimelinesaanroepen na wijziging - ✅ Verplichte
.containerBackgroundtoevoegen voor iOS 17+ - ✅ Vloeiende overgangsanimaties implementeren
- ✅ Laad- en foutstatussen afhandelen
- ✅ Views optimaliseren voor batterijprestaties
- ✅ Testen op alle ondersteunde widgetgroottes
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Tags
Delen
Gerelateerde artikelen

App Intents en Siri Shortcuts: geavanceerde iOS-automatisering 2026
Volledige gids over App Intents en Siri Shortcuts voor iOS 18+. Eigen Siri-acties bouwen, Apple Intelligence integreren en de Swift-app automatiseren in 2026.

Combine vs async/await in Swift: Progressieve Migratiepatronen
Volledige gids voor migratie van Combine naar async/await in Swift: progressieve strategieën, bridging-patronen en paradigma-coëxistentie in iOS-codebases.

iOS-toegankelijkheidsvragen voor sollicitaties in 2026: VoiceOver en Dynamic Type
Bereid je voor op iOS-sollicitaties met essentiële toegankelijkheidsvragen: VoiceOver, Dynamic Type, semantische traits en audits.