# WidgetKit iOS 17+: Interaktive Widgets mit App Intents > Vollständige Anleitung zur Erstellung interaktiver iOS-Widgets mit WidgetKit und App Intents. Buttons, Toggles, Animationen und Best Practices für iOS 17+ in 2026. - Published: 2026-03-04 - Updated: 2026-04-29 - Author: SharpSkill - Tags: widgetkit, ios, app-intents, swift, widgets - Reading time: 14 min --- iOS 17 hat WidgetKit durch die Einführung nativer Interaktivität revolutioniert. Widgets sind keine statischen Anzeigen mehr: Sie können nun direkt vom Home-Bildschirm aus auf Benutzeraktionen reagieren, ohne dass die App geöffnet werden muss. Diese wichtige Weiterentwicklung basiert auf dem App Intents Framework und bietet eine flüssige, moderne Benutzererfahrung. > **Was dieser Artikel behandelt** > > Dieser Artikel zeigt die vollständige Erstellung interaktiver iOS 17+ Widgets, von der Projektkonfiguration bis zu fortgeschrittenen Mustern mit Animationen und Zustandsverwaltung. ## Architektur Interaktiver Widgets Die Interaktivität der iOS 17+ Widgets funktioniert über das App Intents Framework. Im Gegensatz zu traditionellen Deep Links, die die App öffnen würden, ermöglichen App Intents die Code-Ausführung direkt vom Widget aus und aktualisieren anschließend automatisch die Anzeige mit den neuen Daten. ```swift // InteractiveWidgetArchitecture.swift import WidgetKit import SwiftUI import AppIntents // Die Architektur basiert auf drei Hauptkomponenten: // 1. Widget Timeline Provider - liefert die Daten // 2. Widget View - zeigt das Interface mit Button/Toggle // 3. App Intent - führt die Aktion bei Berührung aus struct TaskWidget: Widget { // Eindeutige Widget-Kennung let kind: String = "TaskWidget" var body: some WidgetConfiguration { // StaticConfiguration für Widgets ohne Parameter StaticConfiguration( kind: kind, provider: TaskTimelineProvider() ) { entry in TaskWidgetView(entry: entry) // Erforderlich für App Intents .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Aufgaben") .description("Verwalten Sie Ihre Aufgaben vom Home-Bildschirm aus.") .supportedFamilies([.systemSmall, .systemMedium]) } } ``` Das Widget deklariert seine Konfiguration und gibt den Provider an, der die Daten liefert. Das Attribut `.containerBackground` ist seit iOS 17 für interaktive Widgets verpflichtend. ## Erstellung des Timeline Providers Der Timeline Provider bestimmt, wann und wie das Widget aktualisiert wird. Bei interaktiven Widgets muss er auch auf Änderungen reagieren, die durch App Intents ausgelöst werden. ```swift // TaskTimelineProvider.swift import WidgetKit import SwiftUI // Entry repräsentiert den Widget-Zustand zu einem bestimmten Zeitpunkt struct TaskEntry: TimelineEntry { let date: Date let tasks: [Task] // Ladezustand für visuelles Feedback var isLoading: Bool = false } // Datenmodell, das zwischen App und Widget geteilt wird 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 während des initialen Ladens func placeholder(in context: Context) -> TaskEntry { TaskEntry( date: Date(), tasks: [ Task(id: UUID(), title: "Beispielaufgabe", isCompleted: false, priority: .medium) ] ) } // Snapshot für die Widget-Galerie func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) { let entry = TaskEntry( date: Date(), tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 } ) completion(entry) } // Vollständige Timeline mit Refresh-Policy func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let tasks = TaskDataManager.shared.fetchTasks() let entry = TaskEntry(date: Date(), tasks: Array(tasks.prefix(3))) // Aktualisierung in 15 Minuten oder nach Benutzeraktion let nextUpdate = Calendar.current.date( byAdding: .minute, value: 15, to: Date() ) ?? Date() let timeline = Timeline( entries: [entry], policy: .after(nextUpdate) ) completion(timeline) } } ``` Der Provider verwendet einen gemeinsamen `TaskDataManager` für den Datenzugriff. Dieser Ansatz garantiert die Synchronisation zwischen Hauptanwendung und Widget. > **App Group erforderlich** > > Um Daten zwischen App und Widget zu teilen, muss eine App Group in den Projekt-Capabilities konfiguriert werden. UserDefaults oder Dateien müssen diese gemeinsame Gruppe nutzen. ## Der Manager für Geteilte Daten Der Datenaustausch zwischen Anwendung und Widget erfordert einen gemeinsamen Container, der über App Group zugänglich ist. ```swift // TaskDataManager.swift import Foundation final class TaskDataManager { // Singleton für globalen Zugriff static let shared = TaskDataManager() // App Group Bezeichner, in Xcode konfiguriert private let appGroupID = "group.com.example.taskapp" // UserDefaults zwischen App und Widget geteilt private var sharedDefaults: UserDefaults? { UserDefaults(suiteName: appGroupID) } private let tasksKey = "tasks" private init() {} // Aufgaben aus dem geteilten Speicher abrufen func fetchTasks() -> [Task] { guard let data = sharedDefaults?.data(forKey: tasksKey), let tasks = try? JSONDecoder().decode([Task].self, from: data) else { return [] } return tasks } // Speichern mit Widget-Benachrichtigung func saveTasks(_ tasks: [Task]) { guard let data = try? JSONEncoder().encode(tasks) else { return } sharedDefaults?.set(data, forKey: tasksKey) } // Spezifische Aufgabe aktualisieren func updateTask(_ task: Task) { var tasks = fetchTasks() if let index = tasks.firstIndex(where: { $0.id == task.id }) { tasks[index] = task saveTasks(tasks) } } // Erledigt-Status umschalten func toggleTaskCompletion(taskID: UUID) { var tasks = fetchTasks() if let index = tasks.firstIndex(where: { $0.id == taskID }) { tasks[index].isCompleted.toggle() saveTasks(tasks) } } } ``` Dieser Manager kapselt die gesamte Persistenzlogik und wird sowohl von der Anwendung als auch von den Widget App Intents verwendet. ## Erstellung des App Intent für Interaktivität Der App Intent definiert die Aktion, die bei Benutzerinteraktion mit dem Widget ausgeführt wird. iOS führt diese Aktion im Hintergrund aus und aktualisiert das Widget anschließend automatisch. ```swift // ToggleTaskIntent.swift import AppIntents import WidgetKit // Intent zum Umschalten des Aufgabenstatus struct ToggleTaskIntent: AppIntent { // Titel in Siri-Kurzbefehlen static var title: LocalizedStringResource = "Aufgabenstatus umschalten" // Beschreibung für Barrierefreiheit static var description = IntentDescription("Markiert eine Aufgabe als erledigt oder nicht erledigt.") // Parameter: ID der zu ändernden Aufgabe @Parameter(title: "Aufgaben-ID") var taskID: String // Erforderlicher Initializer für AppIntent init() {} // Initializer mit Parameter zur Erstellung aus der View init(taskID: UUID) { self.taskID = taskID.uuidString } // Ausführung der Aktion func perform() async throws -> some IntentResult { // Konvertierung der String-ID in UUID guard let uuid = UUID(uuidString: taskID) else { return .result() } // Aktualisierung der Aufgabe TaskDataManager.shared.toggleTaskCompletion(taskID: uuid) // Widget-Aktualisierung anfordern WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget") return .result() } } ``` Der Aufruf von `WidgetCenter.shared.reloadTimelines` löst eine sofortige Widget-Aktualisierung nach der Aktion aus und gewährleistet ein unmittelbares visuelles Feedback. ## Widget-View mit Interaktiven Buttons Die Widget-View verwendet die Standard-SwiftUI-Komponente `Button` mit dem Intent als Aktion. iOS 17+ fängt diese Interaktionen automatisch ab, um den App Intent auszuführen. ```swift // TaskWidgetView.swift import SwiftUI import WidgetKit struct TaskWidgetView: View { let entry: TaskEntry // Anpassung an die Widget-Größe @Environment(\.widgetFamily) var family var body: some View { VStack(alignment: .leading, spacing: 8) { // Header mit Titel und Zähler headerView // Aufgabenliste mit interaktiven Buttons ForEach(entry.tasks.prefix(tasksLimit)) { task in TaskRowView(task: task) } Spacer(minLength: 0) } .padding() } // Anzahl der Aufgaben je nach Größe private var tasksLimit: Int { switch family { case .systemSmall: return 2 case .systemMedium: return 3 default: return 4 } } private var headerView: some View { HStack { Text("Aufgaben") .font(.headline) .fontWeight(.bold) Spacer() // Badge mit der Anzahl verbleibender Aufgaben 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 { // Button mit App Intent als Aktion Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // Erledigt-Indikator Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(task.isCompleted ? .green : .secondary) // Aufgabentitel Text(task.title) .font(.subheadline) .strikethrough(task.isCompleted) .foregroundStyle(task.isCompleted ? .secondary : .primary) .lineLimit(1) Spacer() // Prioritätsindikator 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() } } } ``` Die Syntax `Button(intent:)` verbindet den Button direkt mit dem App Intent. Bei Berührung führt iOS `perform()` aus und aktualisiert das Widget anschließend automatisch. ## Interaktiver Toggle für Widgets Für Aktionen vom Typ Ein/Aus bietet die `Toggle`-Komponente eine Alternative zum Button mit nativem iOS-Stil. ```swift // ToggleWidgetView.swift import SwiftUI import AppIntents // Spezifischer Intent für Toggle mit explizitem Status struct SetTaskCompletionIntent: AppIntent { static var title: LocalizedStringResource = "Aufgabenstatus festlegen" @Parameter(title: "Aufgaben-ID") var taskID: String // Zielstatus: true = erledigt, false = nicht erledigt @Parameter(title: "Erledigt") 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 }) { // Status explizit setzen (nicht umschalten) 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() // Interaktiver Toggle mit Intent Toggle( isOn: task.isCompleted, intent: SetTaskCompletionIntent( taskID: task.id, isCompleted: !task.isCompleted ) ) .toggleStyle(.switch) .labelsHidden() } .padding(.vertical, 4) } } ``` Der Toggle bietet eine intuitivere Interaktion für binäre Zustände und integriert sich nahtlos in iOS-Designs. > **Einschränkungen interaktiver Widgets** > > Widgets können keine Alerts, Sheets oder Navigation anzeigen. Alle Aktionen müssen autonom sein und den sichtbaren Status direkt aktualisieren. ## Refresh-Animationen und Übergänge iOS 17+ ermöglicht die Animation von Übergängen während der Widget-Aktualisierung nach einer Aktion. Der Modifier `.contentTransition` steuert diese Animationen. ```swift // AnimatedTaskWidgetView.swift import SwiftUI import WidgetKit struct AnimatedTaskRowView: View { let task: Task var body: some View { Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // Icon mit Übergangsanimation Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(task.isCompleted ? .green : .secondary) // Icon-Animation bei Änderung .contentTransition(.symbolEffect(.replace)) Text(task.title) .font(.subheadline) .strikethrough(task.isCompleted) .foregroundStyle(task.isCompleted ? .secondary : .primary) // Text-Animation .contentTransition(.opacity) Spacer() } .padding(.vertical, 6) .padding(.horizontal, 10) .background( RoundedRectangle(cornerRadius: 8) .fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear) ) // Hintergrund-Animation .animation(.easeInOut(duration: 0.3), value: task.isCompleted) } .buttonStyle(.plain) } } // Widget mit animierter Invalidierung 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("Animierte Aufgaben") .description("Widgets mit weichen Animationen.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) // Aktivierung der Inhaltsanimationen .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("Aufgaben") .font(.headline.bold()) Spacer() let completed = entry.tasks.filter(\.isCompleted).count let total = entry.tasks.count // Animierter Fortschritt Text("\(completed)/\(total)") .font(.caption.bold()) .foregroundStyle(.secondary) .contentTransition(.numericText()) } } } ``` Die Animationen `.symbolEffect(.replace)` und `.numericText()` erzeugen weiche Übergänge zwischen Zuständen und verbessern die Benutzererfahrung erheblich. ## Konfigurierbares Widget mit AppIntentConfiguration Für vom Benutzer anpassbare Widgets (Filter, Kategorien) ersetzt AppIntentConfiguration die StaticConfiguration. ```swift // ConfigurableTaskWidget.swift import WidgetKit import SwiftUI import AppIntents // Dem Benutzer zugängliche Konfiguration struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent { static var title: LocalizedStringResource = "Aufgabenkonfiguration" static var description = IntentDescription("Passen Sie die Aufgabenanzeige an.") // Filter nach Priorität @Parameter(title: "Priorität", default: .all) var priorityFilter: PriorityFilter // Erledigte Aufgaben anzeigen @Parameter(title: "Erledigte anzeigen", default: true) var showCompleted: Bool // Maximale Anzahl von Aufgaben @Parameter(title: "Anzahl Aufgaben", default: 3) var maxTasks: Int } // Enum für Prioritätsfilter enum PriorityFilter: String, AppEnum { case all case high case medium case low static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priorität" static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [ .all: "Alle", .high: "Hoch", .medium: "Mittel", .low: "Niedrig" ] } // An die Konfiguration angepasster Provider 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 { 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)) } // Konfigurationsfilter anwenden private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] { var tasks = TaskDataManager.shared.fetchTasks() // Filterung nach Priorität if config.priorityFilter != .all { let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium tasks = tasks.filter { $0.priority == priority } } // Filterung der Erledigten bei Bedarf if !config.showCompleted { tasks = tasks.filter { !$0.isCompleted } } // Anzahl begrenzen return Array(tasks.prefix(config.maxTasks)) } } // Widget mit Benutzerkonfiguration struct ConfigurableTaskWidget: Widget { let kind: String = "ConfigurableTaskWidget" var body: some WidgetConfiguration { // AppIntentConfiguration für konfigurierbare Widgets AppIntentConfiguration( kind: kind, intent: TaskWidgetConfigurationIntent.self, provider: ConfigurableTaskProvider() ) { entry in TaskWidgetView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Personalisierte Aufgaben") .description("Filtern und personalisieren Sie Ihre Aufgaben.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } } ``` Der Benutzer kann das Widget nun durch langes Drücken konfigurieren und erhält so ein personalisiertes Erlebnis ohne zusätzlichen Code in der Anwendung. ## Fehlerbehandlung und Ladezustände Eine gute UX erfordert die Behandlung von Fehlerfällen und Zwischenzuständen während der Interaktionen. ```swift // TaskIntentWithFeedback.swift import AppIntents import WidgetKit struct ToggleTaskWithFeedbackIntent: AppIntent { static var title: LocalizedStringResource = "Aufgabe mit Feedback umschalten" @Parameter(title: "Aufgaben-ID") var taskID: String init() {} init(taskID: UUID) { self.taskID = taskID.uuidString } func perform() async throws -> some IntentResult & ReturnsValue { guard let uuid = UUID(uuidString: taskID) else { // Stiller Fehler zurückgeben return .result(value: false) } // Asynchrone Operation simulieren (z.B. 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 { // Fehler: Widget nicht aktualisieren return .result(value: false) } } } // View mit Ladezustand struct TaskRowWithLoadingView: View { let task: Task @State private var isLoading = false var body: some View { Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // Bedingter Indikator 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) } } ``` Unmittelbares visuelles Feedback (verringerte Deckkraft, Ladeindikator) signalisiert dem Benutzer, dass seine Aktion registriert wurde. ## Best Practices und Optimierungen Mehrere Muster gewährleisten leistungsstarke und zuverlässige interaktive Widgets. ```swift // WidgetBestPractices.swift import WidgetKit import SwiftUI // 1. Cache nach jeder Änderung immer invalidieren final class WidgetRefreshManager { static func refreshAllWidgets() { // Aktualisierung aller App-Widgets WidgetCenter.shared.reloadAllTimelines() } static func refreshWidget(kind: String) { // Aktualisierung eines bestimmten Widgets WidgetCenter.shared.reloadTimelines(ofKind: kind) } // Aufruf aus der App nach Datenänderung static func notifyDataChanged() { Task { @MainActor in refreshAllWidgets() } } } // 2. Komplexität der Views begrenzen struct OptimizedWidgetView: View { let entry: TaskEntry var body: some View { // Einfache Views ohne GeometryReader bevorzugen VStack(alignment: .leading, spacing: 8) { ForEach(entry.tasks.prefix(3)) { task in // Leichtgewichtige Komponenten minimalTaskRow(task) } } .padding() } // Minimale View für Performance @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. @AppStorage für einfache Zustände verwenden struct QuickSettingsWidgetView: View { // Direkter Zugriff auf geteilte UserDefaults @AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app")) private var showCompleted = true var body: some View { // Status bleibt zwischen Aktualisierungen erhalten Text(showCompleted ? "Alle anzeigen" : "Erledigte ausblenden") } } // 4. Daten im Provider vorberechnen struct OptimizedTaskEntry: TimelineEntry { let date: Date let tasks: [Task] // Vorberechnete Daten let completedCount: Int let pendingCount: Int let highPriorityCount: Int init(date: Date, tasks: [Task]) { self.date = date self.tasks = tasks // Berechnungen werden nur einmal durchgeführt self.completedCount = tasks.filter(\.isCompleted).count self.pendingCount = tasks.filter { !$0.isCompleted }.count self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count } } ``` Diese Optimierungen sorgen für reaktionsschnelle Widgets, die den Akku nicht übermäßig belasten. > **Widget-Debugging** > > Verwenden Sie das Widget-Schema in Xcode für das Debugging. Die Canvas-Vorschau ermöglicht das Testen verschiedener Größen und Zustände ohne Geräteinstallation. ## Fazit WidgetKit iOS 17+ mit App Intents verwandelt Widgets in echte interaktive Erweiterungen von iOS-Anwendungen. Diese deklarative Architektur vereinfacht die Entwicklung erheblich und bietet eine native, flüssige Benutzererfahrung. ### Checkliste Interaktives iOS 17+ Widget - ✅ App Group für Datenaustausch konfigurieren - ✅ Timeline Provider mit angemessener Aktualisierung erstellen - ✅ App Intents für jede Aktion implementieren - ✅ `Button(intent:)` oder `Toggle(intent:)` für Interaktivität verwenden - ✅ `WidgetCenter.shared.reloadTimelines` nach jeder Änderung aufrufen - ✅ Pflicht-`.containerBackground` für iOS 17+ hinzufügen - ✅ Weiche Übergangsanimationen implementieren - ✅ Lade- und Fehlerzustände behandeln - ✅ Views für Akkuleistung optimieren - ✅ Auf allen unterstützten Widget-Größen testen --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/ios/widgetkit-ios17-interactive-widgets-app-intents