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.

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.
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.
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.
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<TaskEntry>) -> 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.
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.
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.
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.
Pronto para mandar bem nas entrevistas de iOS?
Pratique com nossos simuladores interativos, flashcards e testes tecnicos.
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.
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.
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.
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.
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.
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<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))
}
// 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.
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<Bool> {
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.
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.
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:)ouToggle(intent:)para interatividade - ✅ Chamar
WidgetCenter.shared.reloadTimelinesapós modificação - ✅ Adicionar o
.containerBackgroundobrigató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
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Tags
Compartilhar
Artigos relacionados

App Intents e Siri Shortcuts: automação avançada de iOS 2026
Guia completo de App Intents e Siri Shortcuts para iOS 18+. Criar ações personalizadas para Siri, integrar Apple Intelligence e automatizar o app Swift em 2026.

Combine vs async/await em Swift: Padrões de Migração Progressiva
Guia completo para migrar de Combine para async/await em Swift: estratégias progressivas, padrões de ponte e coexistência de paradigmas em bases de código iOS.

Perguntas de entrevista sobre acessibilidade iOS em 2026: VoiceOver e Dynamic Type
Prepare-se para entrevistas iOS com perguntas-chave de acessibilidade: VoiceOver, Dynamic Type, traits semânticos e auditorias.