# WidgetKit iOS 17+: Widgets Interativos com App Intents > Guia completo para criar widgets iOS interativos com WidgetKit e App Intents. Botões, toggles, animações e melhores práticas para iOS 17+ em 2026. - Published: 2026-03-04 - Updated: 2026-04-29 - Author: SharpSkill - Tags: widgetkit, ios, app-intents, swift, widgets - Reading time: 14 min --- O iOS 17 revolucionou o WidgetKit ao introduzir interatividade nativa. Os widgets já não são exibições estáticas: agora podem responder às ações do utilizador diretamente na tela inicial, sem abrir o aplicativo. Esta evolução importante baseia-se no framework App Intents e oferece uma experiência de utilizador fluida e moderna. > **O que este artigo aborda** > > Este artigo apresenta a criação completa de widgets interativos iOS 17+, desde a configuração do projeto até padrões avançados com animações e gerenciamento de estado. ## Arquitetura dos Widgets Interativos A interatividade dos widgets iOS 17+ funciona através do framework App Intents. Diferentemente dos deep links tradicionais que abririam o aplicativo, os App Intents permitem executar código diretamente do widget, e em seguida atualizar automaticamente a exibição com os novos dados. ```swift // InteractiveWidgetArchitecture.swift import WidgetKit import SwiftUI import AppIntents // A arquitetura baseia-se em três componentes principais: // 1. Widget Timeline Provider - fornece os dados // 2. Widget View - exibe a interface com Button/Toggle // 3. App Intent - executa a ação ao toque struct TaskWidget: Widget { // Identificador único do widget let kind: String = "TaskWidget" var body: some WidgetConfiguration { // StaticConfiguration para widgets sem parâmetros StaticConfiguration( kind: kind, provider: TaskTimelineProvider() ) { entry in TaskWidgetView(entry: entry) // Obrigatório para os App Intents .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Tarefas") .description("Gerencie suas tarefas a partir da tela inicial.") .supportedFamilies([.systemSmall, .systemMedium]) } } ``` O widget declara sua configuração e especifica o provider que fornecerá os dados. O atributo `.containerBackground` é obrigatório desde o iOS 17 para os widgets interativos. ## Criação do Timeline Provider O Timeline Provider determina quando e como o widget é atualizado. Para os widgets interativos, ele também deve reagir às mudanças causadas pelos App Intents. ```swift // TaskTimelineProvider.swift import WidgetKit import SwiftUI // Entry que representa o estado do widget num dado momento struct TaskEntry: TimelineEntry { let date: Date let tasks: [Task] // Estado de carregamento para o feedback visual var isLoading: Bool = false } // Modelo de dados compartilhado entre 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 exibido durante o carregamento inicial func placeholder(in context: Context) -> TaskEntry { TaskEntry( date: Date(), tasks: [ Task(id: UUID(), title: "Tarefa de exemplo", isCompleted: false, priority: .medium) ] ) } // Snapshot para a galeria de widgets 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 com política de atualização func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { let tasks = TaskDataManager.shared.fetchTasks() let entry = TaskEntry(date: Date(), tasks: Array(tasks.prefix(3))) // Atualização em 15 minutos ou após uma ação do utilizador let nextUpdate = Calendar.current.date( byAdding: .minute, value: 15, to: Date() ) ?? Date() let timeline = Timeline( entries: [entry], policy: .after(nextUpdate) ) completion(timeline) } } ``` O provider utiliza um `TaskDataManager` compartilhado para acessar os dados. Esta abordagem garante a sincronização entre a aplicação principal e o widget. > **App Group obrigatório** > > Para compartilhar dados entre o app e o widget, é preciso configurar um App Group nas capabilities do projeto. Os UserDefaults ou arquivos devem usar este grupo compartilhado. ## O Gerenciador de Dados Compartilhados O compartilhamento de dados entre a aplicação e o widget requer um contêiner comum acessível via App Group. ```swift // TaskDataManager.swift import Foundation final class TaskDataManager { // Singleton para acesso global static let shared = TaskDataManager() // Identificador do App Group configurado no Xcode private let appGroupID = "group.com.example.taskapp" // UserDefaults compartilhado entre app e widget private var sharedDefaults: UserDefaults? { UserDefaults(suiteName: appGroupID) } private let tasksKey = "tasks" private init() {} // Recupera as tarefas do armazenamento compartilhado func fetchTasks() -> [Task] { guard let data = sharedDefaults?.data(forKey: tasksKey), let tasks = try? JSONDecoder().decode([Task].self, from: data) else { return [] } return tasks } // Salva com notificação ao widget func saveTasks(_ tasks: [Task]) { guard let data = try? JSONEncoder().encode(tasks) else { return } sharedDefaults?.set(data, forKey: tasksKey) } // Atualiza uma tarefa específica func updateTask(_ task: Task) { var tasks = fetchTasks() if let index = tasks.firstIndex(where: { $0.id == task.id }) { tasks[index] = task saveTasks(tasks) } } // Alterna o estado de concluído func toggleTaskCompletion(taskID: UUID) { var tasks = fetchTasks() if let index = tasks.firstIndex(where: { $0.id == taskID }) { tasks[index].isCompleted.toggle() saveTasks(tasks) } } } ``` Este gerenciador encapsula toda a lógica de persistência e será utilizado tanto pela aplicação quanto pelos App Intents do widget. ## Criação do App Intent para Interatividade O App Intent define a ação executada quando o utilizador interage com o widget. O iOS executa esta ação em segundo plano e em seguida atualiza o widget automaticamente. ```swift // ToggleTaskIntent.swift import AppIntents import WidgetKit // Intent para alternar o estado de uma tarefa struct ToggleTaskIntent: AppIntent { // Título exibido nos atalhos da Siri static var title: LocalizedStringResource = "Alternar estado da tarefa" // Descrição para acessibilidade static var description = IntentDescription("Marca uma tarefa como concluída ou não concluída.") // Parâmetro: ID da tarefa a modificar @Parameter(title: "ID da tarefa") var taskID: String // Inicializador requerido pelo AppIntent init() {} // Inicializador com parâmetro para criação a partir da view init(taskID: UUID) { self.taskID = taskID.uuidString } // Execução da ação func perform() async throws -> some IntentResult { // Conversão do ID string para UUID guard let uuid = UUID(uuidString: taskID) else { return .result() } // Atualização da tarefa TaskDataManager.shared.toggleTaskCompletion(taskID: uuid) // Solicita a atualização do widget WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget") return .result() } } ``` A chamada a `WidgetCenter.shared.reloadTimelines` provoca uma atualização imediata do widget após a ação, garantindo um feedback visual instantâneo. ## View do Widget com Botões Interativos A view do widget utiliza o componente `Button` padrão do SwiftUI com o intent como ação. O iOS 17+ intercepta automaticamente estas interações para executar o App Intent. ```swift // TaskWidgetView.swift import SwiftUI import WidgetKit struct TaskWidgetView: View { let entry: TaskEntry // Adaptação ao tamanho do widget @Environment(\.widgetFamily) var family var body: some View { VStack(alignment: .leading, spacing: 8) { // Cabeçalho com título e contador headerView // Lista de tarefas com botões interativos ForEach(entry.tasks.prefix(tasksLimit)) { task in TaskRowView(task: task) } Spacer(minLength: 0) } .padding() } // Número de tarefas conforme o tamanho private var tasksLimit: Int { switch family { case .systemSmall: return 2 case .systemMedium: return 3 default: return 4 } } private var headerView: some View { HStack { Text("Tarefas") .font(.headline) .fontWeight(.bold) Spacer() // Badge com o número de tarefas restantes 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 { // Botão com App Intent como ação Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // Indicador de conclusão Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(task.isCompleted ? .green : .secondary) // Título da tarefa Text(task.title) .font(.subheadline) .strikethrough(task.isCompleted) .foregroundStyle(task.isCompleted ? .secondary : .primary) .lineLimit(1) Spacer() // Indicador de prioridade 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() } } } ``` A sintaxe `Button(intent:)` conecta diretamente o botão ao App Intent. Ao tocar, o iOS executa `perform()` e em seguida atualiza automaticamente o widget. ## Toggle Interativo para os Widgets Para as ações do tipo ligado/desligado, o componente `Toggle` oferece uma alternativa ao botão com um estilo nativo do iOS. ```swift // ToggleWidgetView.swift import SwiftUI import AppIntents // Intent específico para Toggle com estado explícito struct SetTaskCompletionIntent: AppIntent { static var title: LocalizedStringResource = "Definir estado da tarefa" @Parameter(title: "ID da tarefa") var taskID: String // Estado desejado: true = concluída, false = não concluída @Parameter(title: "Concluída") 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 }) { // Define o estado de forma explícita (não 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 interativo com intent Toggle( isOn: task.isCompleted, intent: SetTaskCompletionIntent( taskID: task.id, isCompleted: !task.isCompleted ) ) .toggleStyle(.switch) .labelsHidden() } .padding(.vertical, 4) } } ``` O Toggle oferece uma interação mais intuitiva para os estados binários e integra-se naturalmente nos designs iOS. > **Limitações dos widgets interativos** > > Os widgets não podem exibir alertas, sheets ou navegação. Todas as ações devem ser autônomas e atualizar diretamente o estado visível. ## Animações de Atualização e Transições O iOS 17+ permite animar as transições durante a atualização do widget após uma ação. O modificador `.contentTransition` controla estas animações. ```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) { // Ícone com animação de transição Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle") .font(.title3) .foregroundStyle(task.isCompleted ? .green : .secondary) // Animação do ícone na mudança .contentTransition(.symbolEffect(.replace)) Text(task.title) .font(.subheadline) .strikethrough(task.isCompleted) .foregroundStyle(task.isCompleted ? .secondary : .primary) // Animação do texto .contentTransition(.opacity) Spacer() } .padding(.vertical, 6) .padding(.horizontal, 10) .background( RoundedRectangle(cornerRadius: 8) .fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear) ) // Animação do fundo .animation(.easeInOut(duration: 0.3), value: task.isCompleted) } .buttonStyle(.plain) } } // Widget com invalidação animada 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("Tarefas Animadas") .description("Widgets com animações suaves.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) // Ativação das animações de conteúdo .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("Tarefas") .font(.headline.bold()) Spacer() let completed = entry.tasks.filter(\.isCompleted).count let total = entry.tasks.count // Progresso animado Text("\(completed)/\(total)") .font(.caption.bold()) .foregroundStyle(.secondary) .contentTransition(.numericText()) } } } ``` As animações `.symbolEffect(.replace)` e `.numericText()` criam transições suaves entre os estados, melhorando significativamente a experiência do utilizador. ## Widget Configurável com AppIntentConfiguration Para os widgets personalizáveis pelo utilizador (filtros, categorias), o AppIntentConfiguration substitui o StaticConfiguration. ```swift // ConfigurableTaskWidget.swift import WidgetKit import SwiftUI import AppIntents // Configuração exposta ao utilizador struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent { static var title: LocalizedStringResource = "Configuração de tarefas" static var description = IntentDescription("Personalize a exibição de tarefas.") // Filtro por prioridade @Parameter(title: "Prioridade", default: .all) var priorityFilter: PriorityFilter // Mostrar tarefas concluídas @Parameter(title: "Mostrar concluídas", default: true) var showCompleted: Bool // Número máximo de tarefas @Parameter(title: "Número de tarefas", default: 3) var maxTasks: Int } // Enum para o filtro de prioridade enum PriorityFilter: String, AppEnum { case all case high case medium case low static var typeDisplayRepresentation: TypeDisplayRepresentation = "Prioridade" static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [ .all: "Todas", .high: "Alta", .medium: "Média", .low: "Baixa" ] } // Provider adaptado à configuração 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)) } // Aplica os filtros de configuração private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] { var tasks = TaskDataManager.shared.fetchTasks() // Filtragem por prioridade if config.priorityFilter != .all { let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium tasks = tasks.filter { $0.priority == priority } } // Filtragem das concluídas se necessário if !config.showCompleted { tasks = tasks.filter { !$0.isCompleted } } // Limitação do número return Array(tasks.prefix(config.maxTasks)) } } // Widget com configuração do utilizador struct ConfigurableTaskWidget: Widget { let kind: String = "ConfigurableTaskWidget" var body: some WidgetConfiguration { // AppIntentConfiguration para os widgets configuráveis AppIntentConfiguration( kind: kind, intent: TaskWidgetConfigurationIntent.self, provider: ConfigurableTaskProvider() ) { entry in TaskWidgetView(entry: entry) .containerBackground(.fill.tertiary, for: .widget) } .configurationDisplayName("Tarefas Personalizadas") .description("Filtre e personalize suas tarefas.") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } } ``` O utilizador agora pode configurar o widget com um toque longo, oferecendo uma experiência personalizada sem código adicional na aplicação. ## Tratamento de Erros e Estados de Carregamento Uma boa UX requer tratar os casos de erro e os estados intermediários durante as interações. ```swift // TaskIntentWithFeedback.swift import AppIntents import WidgetKit struct ToggleTaskWithFeedbackIntent: AppIntent { static var title: LocalizedStringResource = "Alternar tarefa com feedback" @Parameter(title: "ID da tarefa") 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 { // Retorna falha silenciosa return .result(value: false) } // Simula uma operação assíncrona (sincronização com servidor por exemplo) do { try await Task.sleep(for: .milliseconds(100)) TaskDataManager.shared.toggleTaskCompletion(taskID: uuid) WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget") return .result(value: true) } catch { // Erro: não atualizar o widget return .result(value: false) } } } // View com estado de carregamento struct TaskRowWithLoadingView: View { let task: Task @State private var isLoading = false var body: some View { Button(intent: ToggleTaskIntent(taskID: task.id)) { HStack(spacing: 12) { // Indicador condicional 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) } } ``` Um feedback visual imediato (opacidade reduzida, indicador de carregamento) informa o utilizador de que sua ação foi registrada. ## Melhores Práticas e Otimizações Vários padrões garantem widgets interativos eficientes e confiáveis. ```swift // WidgetBestPractices.swift import WidgetKit import SwiftUI // 1. Sempre invalidar o cache após uma modificação final class WidgetRefreshManager { static func refreshAllWidgets() { // Atualização de todos os widgets do app WidgetCenter.shared.reloadAllTimelines() } static func refreshWidget(kind: String) { // Atualização de um widget específico WidgetCenter.shared.reloadTimelines(ofKind: kind) } // Chamada a partir do app após modificação dos dados static func notifyDataChanged() { Task { @MainActor in refreshAllWidgets() } } } // 2. Limitar a complexidade das views struct OptimizedWidgetView: View { let entry: TaskEntry var body: some View { // Preferir views simples sem GeometryReader VStack(alignment: .leading, spacing: 8) { ForEach(entry.tasks.prefix(3)) { task in // Componentes leves minimalTaskRow(task) } } .padding() } // View mínima para o desempenho @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. Usar @AppStorage para os estados simples struct QuickSettingsWidgetView: View { // Acesso direto aos UserDefaults compartilhados @AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app")) private var showCompleted = true var body: some View { // O estado persiste entre as atualizações Text(showCompleted ? "Mostrando todas" : "Ocultando concluídas") } } // 4. Pré-calcular os dados no provider struct OptimizedTaskEntry: TimelineEntry { let date: Date let tasks: [Task] // Dados pré-calculados let completedCount: Int let pendingCount: Int let highPriorityCount: Int init(date: Date, tasks: [Task]) { self.date = date self.tasks = tasks // Cálculos efetuados uma única vez self.completedCount = tasks.filter(\.isCompleted).count self.pendingCount = tasks.filter { !$0.isCompleted }.count self.highPriorityCount = tasks.filter { $0.priority == .high && !$0.isCompleted }.count } } ``` Estas otimizações garantem widgets responsivos que não consomem excessivamente a bateria. > **Depuração dos widgets** > > Usar o scheme widget no Xcode para a depuração. O preview canvas permite testar os diferentes tamanhos e estados sem instalação no dispositivo. ## Conclusão O WidgetKit iOS 17+ com App Intents transforma os widgets em verdadeiras extensões interativas das aplicações iOS. Esta arquitetura declarativa simplifica consideravelmente o desenvolvimento oferecendo uma experiência de utilizador nativa e fluida. ### Checklist Widget Interativo iOS 17+ - ✅ Configurar um App Group para compartilhamento de dados - ✅ Criar um Timeline Provider com atualização apropriada - ✅ Implementar App Intents para cada ação - ✅ Usar `Button(intent:)` ou `Toggle(intent:)` para interatividade - ✅ Chamar `WidgetCenter.shared.reloadTimelines` após modificação - ✅ Adicionar o `.containerBackground` obrigatório para iOS 17+ - ✅ Implementar animações de transição suaves - ✅ Tratar os estados de carregamento e erro - ✅ Otimizar as views para o desempenho da bateria - ✅ Testar em todos os tamanhos de widget compatíveis --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/ios/widgetkit-ios17-interactive-widgets-app-intents