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.

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.
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.
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.
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<TaskEntry>) -> 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.
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.
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.
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.
Pronto a superare i tuoi colloqui su iOS?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
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.
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.
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.
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.
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.
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<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))
}
// 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.
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<Bool> {
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.
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.
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:)oToggle(intent:)per l'interattività - ✅ Chiamare
WidgetCenter.shared.reloadTimelinesdopo ogni modifica - ✅ Aggiungere il
.containerBackgroundobbligatorio 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
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

App Intents e Siri Shortcuts: automazione iOS avanzata 2026
Guida completa ad App Intents e Siri Shortcuts per iOS 18+. Creare azioni personalizzate per Siri, integrare Apple Intelligence e automatizzare l'app Swift nel 2026.

Combine vs async/await in Swift: Pattern di Migrazione Progressiva
Guida completa alla migrazione da Combine ad async/await in Swift: strategie progressive, pattern di bridging e coesistenza dei paradigmi nelle codebase iOS.

Domande di colloquio sull'accessibilità iOS nel 2026: VoiceOver e Dynamic Type
Preparati ai colloqui iOS con domande chiave sull'accessibilità: VoiceOver, Dynamic Type, trait semantici e audit.