# WidgetKit iOS 17+: Widget Interattivi con App Intents > Guida completa per creare widget iOS interattivi con WidgetKit e App Intents. Pulsanti, toggle, animazioni e best practice per iOS 17+ nel 2026. - Published: 2026-03-04 - Updated: 2026-04-29 - Author: SharpSkill - Tags: widgetkit, ios, app-intents, swift, widgets - Reading time: 14 min --- iOS 17 ha rivoluzionato WidgetKit introducendo l'interattività nativa. I widget non sono più semplici visualizzazioni statiche: ora possono rispondere alle azioni dell'utente direttamente dalla schermata Home, senza aprire l'app. Questa importante evoluzione si basa sul framework App Intents e offre un'esperienza utente fluida e moderna. > **Cosa copre questo articolo** > > Questo articolo presenta la creazione completa di widget interattivi iOS 17+, dalla configurazione del progetto ai pattern avanzati con animazioni e gestione dello stato. ## Architettura dei Widget Interattivi L'interattività dei widget iOS 17+ funziona tramite il framework App Intents. A differenza dei deep link tradizionali che aprirebbero l'app, gli App Intents permettono di eseguire codice direttamente dal widget, aggiornando poi automaticamente la visualizzazione con i nuovi dati. ```swift // InteractiveWidgetArchitecture.swift import WidgetKit import SwiftUI import AppIntents // L'architettura si basa su tre componenti principali: // 1. Widget Timeline Provider - fornisce i dati // 2. Widget View - mostra l'interfaccia con Button/Toggle // 3. App Intent - esegue l'azione al tocco struct TaskWidget: Widget { // Identificatore univoco del widget let kind: String = "TaskWidget" var body: some WidgetConfiguration { // StaticConfiguration per widget senza parametri StaticConfiguration( kind: kind, provider: TaskTimelineProvider() ) { entry in TaskWidgetView(entry: entry) // Obbligatorio per gli App Intents .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Attività") .description("Gestisci le tue attività dalla schermata Home.") .supportedFamilies([.systemSmall, .systemMedium]) } } ``` Il widget dichiara la sua configurazione e specifica il provider che fornirà i dati. L'attributo `.containerBackground` è obbligatorio da iOS 17 per i widget interattivi. ## Creazione del Timeline Provider Il Timeline Provider determina quando e come il widget si aggiorna. Per i widget interattivi, deve anche reagire ai cambiamenti causati dagli App Intents. ```swift // TaskTimelineProvider.swift import WidgetKit import SwiftUI // Entry che rappresenta lo stato del widget in un dato momento struct TaskEntry: TimelineEntry { let date: Date let tasks: [Task] // Stato di caricamento per il feedback visivo var isLoading: Bool = false } // Modello dati condiviso tra app e 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 mostrato durante il caricamento iniziale func placeholder(in context: Context) -> TaskEntry { TaskEntry( date: Date(), tasks: [ Task(id: UUID(), title: "Attività di esempio", isCompleted: false, priority: .medium) ] ) } // Snapshot per la galleria dei widget func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) { let entry = TaskEntry( date: Date(), tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { $0 } ) completion(entry) } // Timeline completa con policy di aggiornamento func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let tasks = TaskDataManager.shared.fetchTasks() let entry = TaskEntry(date: Date(), tasks: Array(tasks.prefix(3))) // Aggiornamento tra 15 minuti o dopo un'azione utente let nextUpdate = Calendar.current.date( byAdding: .minute, value: 15, to: Date() ) ?? Date() let timeline = Timeline( entries: [entry], policy: .after(nextUpdate) ) completion(timeline) } } ``` Il provider utilizza un `TaskDataManager` condiviso per accedere ai dati. Questo approccio garantisce la sincronizzazione tra l'applicazione principale e il widget. > **App Group obbligatorio** > > Per condividere dati tra app e widget, è necessario configurare un App Group nelle capabilities del progetto. UserDefaults o file devono utilizzare questo gruppo condiviso. ## Il Gestore di Dati Condivisi La condivisione dei dati tra applicazione e widget richiede un container comune accessibile tramite App Group. ```swift // TaskDataManager.swift import Foundation final class TaskDataManager { // Singleton per accesso globale static let shared = TaskDataManager() // Identificatore App Group configurato in Xcode private let appGroupID = "group.com.example.taskapp" // UserDefaults condiviso tra app e widget private var sharedDefaults: UserDefaults? { UserDefaults(suiteName: appGroupID) } private let tasksKey = "tasks" private init() {} // Recupera le attività dalla memoria condivisa func fetchTasks() -> [Task] { guard let data = sharedDefaults?.data(forKey: tasksKey), let tasks = try? JSONDecoder().decode([Task].self, from: data) else { return [] } return tasks } // Salva con notifica al widget func saveTasks(_ tasks: [Task]) { guard let data = try? JSONEncoder().encode(tasks) else { return } sharedDefaults?.set(data, forKey: tasksKey) } // Aggiorna un'attività specifica func updateTask(_ task: Task) { var tasks = fetchTasks() if let index = tasks.firstIndex(where: { $0.id == task.id }) { tasks[index] = task saveTasks(tasks) } } // Inverte lo stato di completamento func toggleTaskCompletion(taskID: UUID) { var tasks = fetchTasks() if let index = tasks.firstIndex(where: { $0.id == taskID }) { tasks[index].isCompleted.toggle() saveTasks(tasks) } } } ``` Questo gestore incapsula tutta la logica di persistenza e sarà utilizzato sia dall'applicazione che dagli App Intents del widget. ## Creazione dell'App Intent per l'Interattività L'App Intent definisce l'azione eseguita quando l'utente interagisce con il widget. iOS esegue questa azione in background e poi aggiorna automaticamente il widget. ```swift // ToggleTaskIntent.swift import AppIntents import WidgetKit // Intent per invertire lo stato di un'attività struct ToggleTaskIntent: AppIntent { // Titolo visualizzato nelle scorciatoie Siri static var title: LocalizedStringResource = "Inverti stato attività" // Descrizione per accessibilità static var description = IntentDescription("Contrassegna un'attività come completata o non completata.") // Parametro: ID dell'attività da modificare @Parameter(title: "ID attività") var taskID: String // Inizializzatore richiesto da AppIntent init() {} // Inizializzatore con parametro per la creazione dalla view init(taskID: UUID) { self.taskID = taskID.uuidString } // Esecuzione dell'azione func perform() async throws -> some IntentResult { // Conversione dell'ID stringa in UUID guard let uuid = UUID(uuidString: taskID) else { return .result() } // Aggiornamento dell'attività TaskDataManager.shared.toggleTaskCompletion(taskID: uuid) // Richiede l'aggiornamento del widget WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget") return .result() } } ``` La chiamata a `WidgetCenter.shared.reloadTimelines` provoca un aggiornamento immediato del widget dopo l'azione, garantendo un feedback visivo istantaneo. ## View del Widget con Pulsanti Interattivi La view del widget utilizza il componente `Button` standard di SwiftUI con l'intent come azione. iOS 17+ intercetta automaticamente queste interazioni per eseguire l'App Intent. ```swift // TaskWidgetView.swift import SwiftUI import WidgetKit struct TaskWidgetView: View { let entry: TaskEntry // Adattamento alla dimensione del widget @Environment(\.widgetFamily) var family var body: some View { VStack(alignment: .leading, spacing: 8) { // Header con titolo e contatore headerView // Lista attività con pulsanti interattivi ForEach(entry.tasks.prefix(tasksLimit)) { task in TaskRowView(task: task) } Spacer(minLength: 0) } .padding() } // Numero di attività in base alla dimensione private var tasksLimit: Int { switch family { case .systemSmall: return 2 case .systemMedium: return 3 default: return 4 } } private var headerView: some View { HStack { Text("Attività") .font(.headline) .fontWeight(.bold) Spacer() // Badge con il numero di attività rimanenti 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 { // Pulsante con App Intent come azione Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // Indicatore di completamento Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(task.isCompleted ? .green : .secondary) // Titolo dell'attività Text(task.title) .font(.subheadline) .strikethrough(task.isCompleted) .foregroundStyle(task.isCompleted ? .secondary : .primary) .lineLimit(1) Spacer() // Indicatore di priorità 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() } } } ``` La sintassi `Button(intent:)` collega direttamente il pulsante all'App Intent. Al tocco, iOS esegue `perform()` e poi aggiorna automaticamente il widget. ## Toggle Interattivo per i Widget Per le azioni di tipo on/off, il componente `Toggle` offre un'alternativa al pulsante con uno stile nativo iOS. ```swift // ToggleWidgetView.swift import SwiftUI import AppIntents // Intent specifico per Toggle con stato esplicito struct SetTaskCompletionIntent: AppIntent { static var title: LocalizedStringResource = "Imposta stato attività" @Parameter(title: "ID attività") var taskID: String // Stato desiderato: true = completata, false = non completata @Parameter(title: "Completata") 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 }) { // Imposta lo stato in modo esplicito (non 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() // Toggle interattivo con intent Toggle( isOn: task.isCompleted, intent: SetTaskCompletionIntent( taskID: task.id, isCompleted: !task.isCompleted ) ) .toggleStyle(.switch) .labelsHidden() } .padding(.vertical, 4) } } ``` Il Toggle offre un'interazione più intuitiva per gli stati binari e si integra naturalmente nei design iOS. > **Limitazioni dei widget interattivi** > > I widget non possono visualizzare alert, sheet o navigazione. Tutte le azioni devono essere autonome e aggiornare direttamente lo stato visibile. ## Animazioni di Aggiornamento e Transizioni iOS 17+ permette di animare le transizioni durante l'aggiornamento del widget dopo un'azione. Il modificatore `.contentTransition` controlla queste animazioni. ```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) { // Icona con animazione di transizione Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(task.isCompleted ? .green : .secondary) // Animazione dell'icona al cambiamento .contentTransition(.symbolEffect(.replace)) Text(task.title) .font(.subheadline) .strikethrough(task.isCompleted) .foregroundStyle(task.isCompleted ? .secondary : .primary) // Animazione del testo .contentTransition(.opacity) Spacer() } .padding(.vertical, 6) .padding(.horizontal, 10) .background( RoundedRectangle(cornerRadius: 8) .fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear) ) // Animazione dello sfondo .animation(.easeInOut(duration: 0.3), value: task.isCompleted) } .buttonStyle(.plain) } } // Widget con invalidazione animata 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("Attività Animate") .description("Widget con animazioni fluide.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) // Attivazione delle animazioni di contenuto .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("Attività") .font(.headline.bold()) Spacer() let completed = entry.tasks.filter(\.isCompleted).count let total = entry.tasks.count // Progresso animato Text("\(completed)/\(total)") .font(.caption.bold()) .foregroundStyle(.secondary) .contentTransition(.numericText()) } } } ``` Le animazioni `.symbolEffect(.replace)` e `.numericText()` creano transizioni fluide tra gli stati, migliorando significativamente l'esperienza utente. ## Widget Configurabile con AppIntentConfiguration Per i widget personalizzabili dall'utente (filtri, categorie), AppIntentConfiguration sostituisce StaticConfiguration. ```swift // ConfigurableTaskWidget.swift import WidgetKit import SwiftUI import AppIntents // Configurazione esposta all'utente struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent { static var title: LocalizedStringResource = "Configurazione attività" static var description = IntentDescription("Personalizza la visualizzazione delle attività.") // Filtro per priorità @Parameter(title: "Priorità", default: .all) var priorityFilter: PriorityFilter // Mostra attività completate @Parameter(title: "Mostra completate", default: true) var showCompleted: Bool // Numero massimo di attività @Parameter(title: "Numero attività", default: 3) var maxTasks: Int } // Enum per il filtro priorità enum PriorityFilter: String, AppEnum { case all case high case medium case low static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priorità" static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [ .all: "Tutte", .high: "Alta", .medium: "Media", .low: "Bassa" ] } // Provider adattato alla configurazione 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)) } // Applica i filtri di configurazione private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] { var tasks = TaskDataManager.shared.fetchTasks() // Filtro per priorità if config.priorityFilter != .all { let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium tasks = tasks.filter { $0.priority == priority } } // Filtra le completate se necessario if !config.showCompleted { tasks = tasks.filter { !$0.isCompleted } } // Limita il numero return Array(tasks.prefix(config.maxTasks)) } } // Widget con configurazione utente struct ConfigurableTaskWidget: Widget { let kind: String = "ConfigurableTaskWidget" var body: some WidgetConfiguration { // AppIntentConfiguration per widget configurabili AppIntentConfiguration( kind: kind, intent: TaskWidgetConfigurationIntent.self, provider: ConfigurableTaskProvider() ) { entry in TaskWidgetView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Attività Personalizzate") .description("Filtra e personalizza le tue attività.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } } ``` L'utente può ora configurare il widget tramite pressione prolungata, offrendo un'esperienza personalizzata senza codice aggiuntivo nell'applicazione. ## Gestione degli Errori e degli Stati di Caricamento Una buona UX richiede di gestire i casi di errore e gli stati intermedi durante le interazioni. ```swift // TaskIntentWithFeedback.swift import AppIntents import WidgetKit struct ToggleTaskWithFeedbackIntent: AppIntent { static var title: LocalizedStringResource = "Inverti attività con feedback" @Parameter(title: "ID attività") 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 { // Restituisce errore silenzioso return .result(value: false) } // Simula un'operazione asincrona (sync server per esempio) do { try await Task.sleep(for: .milliseconds(100)) TaskDataManager.shared.toggleTaskCompletion(taskID: uuid) WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget") return .result(value: true) } catch { // Errore: non aggiornare il widget return .result(value: false) } } } // View con stato di caricamento struct TaskRowWithLoadingView: View { let task: Task @State private var isLoading = false var body: some View { Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // Indicatore condizionale 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) } } ``` Un feedback visivo immediato (opacità ridotta, indicatore di caricamento) informa l'utente che la sua azione è stata registrata. ## Best Practice e Ottimizzazioni Diversi pattern garantiscono widget interattivi performanti e affidabili. ```swift // WidgetBestPractices.swift import WidgetKit import SwiftUI // 1. Invalidare sempre la cache dopo una modifica final class WidgetRefreshManager { static func refreshAllWidgets() { // Aggiornamento di tutti i widget dell'app WidgetCenter.shared.reloadAllTimelines() } static func refreshWidget(kind: String) { // Aggiornamento di un widget specifico WidgetCenter.shared.reloadTimelines(ofKind: kind) } // Chiamata dall'app dopo modifica dei dati static func notifyDataChanged() { Task { @MainActor in refreshAllWidgets() } } } // 2. Limitare la complessità delle view struct OptimizedWidgetView: View { let entry: TaskEntry var body: some View { // Preferire view semplici senza GeometryReader VStack(alignment: .leading, spacing: 8) { ForEach(entry.tasks.prefix(3)) { task in // Componenti leggeri minimalTaskRow(task) } } .padding() } // View minimale per le 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. Usare @AppStorage per stati semplici struct QuickSettingsWidgetView: View { // Accesso diretto agli UserDefaults condivisi @AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app")) private var showCompleted = true var body: some View { // Lo stato persiste tra gli aggiornamenti Text(showCompleted ? "Mostro tutte" : "Nascondo completate") } } // 4. Pre-calcolare i dati nel provider struct OptimizedTaskEntry: TimelineEntry { let date: Date let tasks: [Task] // Dati pre-calcolati let completedCount: Int let pendingCount: Int let highPriorityCount: Int init(date: Date, tasks: [Task]) { self.date = date self.tasks = tasks // Calcoli effettuati una sola volta self.completedCount = tasks.filter(\.isCompleted).count self.pendingCount = tasks.filter { !$0.isCompleted }.count self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count } } ``` Queste ottimizzazioni garantiscono widget reattivi che non consumano eccessivamente la batteria. > **Debug dei widget** > > Utilizzare lo schema widget in Xcode per il debug. La preview canvas permette di testare diverse dimensioni e stati senza installazione su dispositivo. ## Conclusione WidgetKit iOS 17+ con App Intents trasforma i widget in vere estensioni interattive delle applicazioni iOS. Questa architettura dichiarativa semplifica notevolmente lo sviluppo offrendo un'esperienza utente nativa e fluida. ### Checklist Widget Interattivo iOS 17+ - ✅ Configurare un App Group per la condivisione dei dati - ✅ Creare un Timeline Provider con aggiornamento appropriato - ✅ Implementare App Intents per ogni azione - ✅ Usare `Button(intent:)` o `Toggle(intent:)` per l'interattività - ✅ Chiamare `WidgetCenter.shared.reloadTimelines` dopo ogni modifica - ✅ Aggiungere il `.containerBackground` obbligatorio per iOS 17+ - ✅ Implementare animazioni di transizione fluide - ✅ Gestire gli stati di caricamento ed errore - ✅ Ottimizzare le view per le performance della batteria - ✅ Testare su tutte le dimensioni di widget supportate --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/ios/widgetkit-ios17-interactive-widgets-app-intents