WidgetKit iOS 17-26: Widgets Interativos com App Intents
Guia completo para criar widgets iOS interativos com WidgetKit e App Intents. Botões, toggles, animações, estilo Liquid Glass do iOS 26 e atualizações do WWDC 2026.

O iOS 17 introduziu interatividade nativa no WidgetKit, permitindo que os widgets respondam às ações do usuário diretamente na tela inicial sem abrir o aplicativo. O iOS 26 aprimorou essa experiência com o material Liquid Glass e modos de renderização acentuada. Este guia aborda o desenvolvimento de widgets interativos do iOS 17 ao iOS 26.
Criação de widgets interativos iOS 17-26, desde a configuração do projeto até padrões avançados com animações, gerenciamento de estado e as últimas funcionalidades de App Intents do WWDC 2026.
Arquitetura de Widgets Interativos no iOS 17+
A interatividade dos widgets iOS 17+ funciona por meio do framework App Intents. Diferentemente dos deep links tradicionais que abririam o aplicativo, os App Intents permitem executar código diretamente do widget, atualizando em seguida automaticamente a exibição com os novos dados.
import WidgetKit
import SwiftUI
import AppIntents
// The architecture relies on three main components:
// 1. Widget Timeline Provider - supplies the data
// 2. Widget View - displays the interface with Button/Toggle
// 3. App Intent - executes the action on tap
struct TaskWidget: Widget {
// Unique widget identifier
let kind: String = "TaskWidget"
var body: some WidgetConfiguration {
// StaticConfiguration for widgets without parameters
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
TaskWidgetView(entry: entry)
// Required for App Intents since iOS 17
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Tasks")
.description("Manage your tasks from the home screen.")
.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 widgets interativos, e o iOS 26 aplica automaticamente o estilo Liquid Glass quando apropriado.
Criação do Timeline Provider
O Timeline Provider determina quando e como o widget é atualizado. Para widgets interativos, ele também deve reagir às mudanças causadas pelos App Intents.
import WidgetKit
import SwiftUI
// Entry representing widget state at a given moment
struct TaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Loading state for visual feedback
var isLoading: Bool = false
}
// Data model shared between app and 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 displayed during initial loading
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(
date: Date(),
tasks: [
Task(id: UUID(), title: "Sample task", isCompleted: false, priority: .medium)
]
)
}
// Snapshot for widget gallery
func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
let entry = TaskEntry(
date: Date(),
tasks: TaskDataManager.shared.fetchTasks().prefix(3).map { /bin/bash }
)
completion(entry)
}
// Complete timeline with refresh policy
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)))
// Refresh in 15 minutes or after user action
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. Essa abordagem garante a sincronização entre a aplicação principal e o widget. Para padrões relacionados de gerenciamento de estado, consulte SwiftUI @Observable vs @State.
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 for global access
static let shared = TaskDataManager()
// App Group identifier configured in Xcode
private let appGroupID = "group.com.example.taskapp"
// UserDefaults shared between app and widget
private var sharedDefaults: UserDefaults? {
UserDefaults(suiteName: appGroupID)
}
private let tasksKey = "tasks"
private init() {}
// Fetch tasks from shared storage
func fetchTasks() -> [Task] {
guard let data = sharedDefaults?.data(forKey: tasksKey),
let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
return []
}
return tasks
}
// Save with widget notification
func saveTasks(_ tasks: [Task]) {
guard let data = try? JSONEncoder().encode(tasks) else { return }
sharedDefaults?.set(data, forKey: tasksKey)
}
// Update a specific task
func updateTask(_ task: Task) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { /bin/bash.id == task.id }) {
tasks[index] = task
saveTasks(tasks)
}
}
// Toggle completed state
func toggleTaskCompletion(taskID: UUID) {
var tasks = fetchTasks()
if let index = tasks.firstIndex(where: { /bin/bash.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 usuário interage com o widget. O iOS executa essa ação em segundo plano e em seguida atualiza o widget automaticamente.
import AppIntents
import WidgetKit
// Intent to toggle task state
struct ToggleTaskIntent: AppIntent {
// Title displayed in Siri shortcuts
static var title: LocalizedStringResource = "Toggle task state"
// Description for accessibility
static var description = IntentDescription("Marks a task as completed or not completed.")
// Parameter: ID of task to modify
@Parameter(title: "Task ID")
var taskID: String
// Required initializer for AppIntent
init() {}
// Initializer with parameter for view creation
init(taskID: UUID) {
self.taskID = taskID.uuidString
}
// Action execution
func perform() async throws -> some IntentResult {
// Convert string ID to UUID
guard let uuid = UUID(uuidString: taskID) else {
return .result()
}
// Update the task
TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
// Request widget refresh
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 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 essas interações para executar o App Intent.
import SwiftUI
import WidgetKit
struct TaskWidgetView: View {
let entry: TaskEntry
// Adapt to widget size
@Environment(.widgetFamily) var family
var body: some View {
VStack(alignment: .leading, spacing: 8) {
// Header with title and counter
headerView
// Task list with interactive buttons
ForEach(entry.tasks.prefix(tasksLimit)) { task in
TaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
// Number of tasks based on size
private var tasksLimit: Int {
switch family {
case .systemSmall: return 2
case .systemMedium: return 3
default: return 4
}
}
private var headerView: some View {
HStack {
Text("Tasks")
.font(.headline)
.fontWeight(.bold)
Spacer()
// Badge with remaining task count
let remaining = entry.tasks.filter { !/bin/bash.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 with App Intent as action
Button(intent: ToggleTaskIntent(taskID: task.id)) {
HStack(spacing: 12) {
// Completion indicator
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Task title
Text(task.title)
.font(.subheadline)
.strikethrough(task.isCompleted)
.foregroundStyle(task.isCompleted ? .secondary : .primary)
.lineLimit(1)
Spacer()
// Priority indicator
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 ações do tipo ligado/desligado, o componente Toggle oferece uma alternativa ao botão com estilo nativo do iOS.
import SwiftUI
import AppIntents
// Specific intent for Toggle with explicit state
struct SetTaskCompletionIntent: AppIntent {
static var title: LocalizedStringResource = "Set task state"
@Parameter(title: "Task ID")
var taskID: String
// Target state: true = completed, false = not completed
@Parameter(title: "Completed")
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: { /bin/bash.id == uuid }) {
// Set state explicitly (not 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()
// Interactive toggle with 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 estados binários e se integra 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 essas 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) {
// Icon with transition animation
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.font(.title3)
.foregroundStyle(task.isCompleted ? .green : .secondary)
// Icon animation on change
.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)
)
// Background animation
.animation(.easeInOut(duration: 0.3), value: task.isCompleted)
}
.buttonStyle(.plain)
}
}
// Widget with animated invalidation
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("Animated Tasks")
.description("Widgets with smooth animations.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
// Enable content animations
.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("Tasks")
.font(.headline.bold())
Spacer()
let completed = entry.tasks.filter(.isCompleted).count
let total = entry.tasks.count
// Animated progress
Text("\(completed)/\(total)")
.font(.caption.bold())
.foregroundStyle(.secondary)
.contentTransition(.numericText())
}
}
}As animações .symbolEffect(.replace) e .numericText() criam transições suaves entre estados, melhorando significativamente a experiência do usuário.
iOS 26 Liquid Glass e Renderização Acentuada
O iOS 26 introduziu o material Liquid Glass e modos de renderização acentuada para os widgets. Essas atualizações visuais são aplicadas automaticamente ao usar .containerBackground, mas os desenvolvedores podem controlar o comportamento da renderização.
import SwiftUI
import WidgetKit
struct ModernTaskWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(entry.tasks.prefix(3)) { task in
TaskRowView(task: task)
}
}
.padding()
// iOS 26: Control accented rendering for images
.widgetAccentedRenderingMode(.fullColor)
}
}
// Extra large portrait family (iOS 26+)
struct LargePortraitTaskWidget: Widget {
let kind: String = "LargePortraitTaskWidget"
var body: some WidgetConfiguration {
StaticConfiguration(
kind: kind,
provider: TaskTimelineProvider()
) { entry in
LargePortraitTaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Task Overview")
.description("Full task overview with extra space.")
// New in iOS 26: system extra large portrait
.supportedFamilies([.systemExtraLarge])
}
}
struct LargePortraitTaskWidgetView: View {
let entry: TaskEntry
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("All Tasks")
.font(.title2.bold())
// More space for task details
ForEach(entry.tasks) { task in
DetailedTaskRowView(task: task)
}
Spacer(minLength: 0)
}
.padding()
}
}
struct DetailedTaskRowView: View {
let task: Task
var body: some View {
Button(intent: ToggleTaskIntent(taskID: task.id)) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
.foregroundStyle(task.isCompleted ? .green : .secondary)
Text(task.title)
.font(.headline)
.strikethrough(task.isCompleted)
}
Text("Priority: \(task.priority.rawValue.capitalized)")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.systemBackground).opacity(0.3))
.cornerRadius(12)
}
.buttonStyle(.plain)
}
}O modificador .widgetAccentedRenderingMode(.fullColor) preserva as cores das imagens em ambientes acentuados. A família .systemExtraLarge, originária do visionOS 26, está agora disponível no iOS e iPadOS.
WWDC 2026 App Intents: EntityCollection para Performance
A WWDC 2026 introduziu o EntityCollection para lidar com grandes quantidades de entidades de forma eficiente. Em vez de resolver todas as entidades antes da execução do intent, apenas os identificadores são passados.
import AppIntents
import WidgetKit
// Mark multiple tasks complete at once - efficiently
struct BatchCompleteTasksIntent: AppIntent {
static var title: LocalizedStringResource = "Complete multiple tasks"
// EntityCollection passes IDs only, not resolved entities
@Parameter(title: "Tasks")
var tasks: EntityCollection<TaskEntity>
init() {}
init(taskIDs: [UUID]) {
// Create collection from IDs
self.tasks = EntityCollection(taskIDs.map { TaskEntity(id: /bin/bash) })
}
func perform() async throws -> some IntentResult {
// Access IDs directly without resolution overhead
let taskIDs = tasks.identifiers
var allTasks = TaskDataManager.shared.fetchTasks()
for id in taskIDs {
if let uuid = UUID(uuidString: id),
let index = allTasks.firstIndex(where: { /bin/bash.id == uuid }) {
allTasks[index].isCompleted = true
}
}
TaskDataManager.shared.saveTasks(allTasks)
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}
// Entity conformance for TaskEntity
struct TaskEntity: AppEntity {
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Task"
static var defaultQuery = TaskEntityQuery()
var id: UUID
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: "Task \(id.uuidString.prefix(8))")
}
}
struct TaskEntityQuery: EntityQuery {
func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
identifiers.map { TaskEntity(id: /bin/bash) }
}
}Esse padrão é crítico para operações em lote. Os benchmarks internos da Apple mostraram que marcar 1000 fotos passou de lento para "quase instantâneo" com EntityCollection.
Intents de Longa Duração com Exibição de Progresso
A WWDC 2026 também introduziu o LongRunningIntent para operações que ultrapassam o limite padrão de 30 segundos, com progresso exibido como Live Activity.
import AppIntents
import WidgetKit
struct SyncAllTasksIntent: LongRunningIntent, CancellableIntent {
static var title: LocalizedStringResource = "Sync all tasks"
func perform() async throws -> some IntentResult & ProvidesDialog {
let result = try await performBackgroundTask {
let tasks = TaskDataManager.shared.fetchTasks()
progress.totalUnitCount = Int64(tasks.count)
for (index, task) in tasks.enumerated() {
// Check for cancellation
try Task.checkCancellation()
// Simulate server sync
try await Task.sleep(for: .milliseconds(100))
progress.completedUnitCount = Int64(index + 1)
}
return "Synced \(tasks.count) tasks"
} onCancel: { reason in
// Cleanup based on cancellation reason
switch reason {
case .user:
print("User cancelled sync")
case .timeout:
print("System timeout")
case .systemReclaim:
print("System reclaimed resources")
@unknown default:
break
}
}
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result(dialog: "\(result)")
}
}O progresso aparece como Live Activity, e o CancellableIntent fornece o motivo do cancelamento para que a limpeza possa ser tratada adequadamente. Para mais informações sobre padrões de concorrência em Swift, consulte Swift Structured Concurrency.
Widget Configurável com AppIntentConfiguration
Para widgets personalizáveis pelo usuário (filtros, categorias), o AppIntentConfiguration substitui o StaticConfiguration.
import WidgetKit
import SwiftUI
import AppIntents
// Configuration exposed to user
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Task configuration"
static var description = IntentDescription("Customize task display.")
// Filter by priority
@Parameter(title: "Priority", default: .all)
var priorityFilter: PriorityFilter
// Show completed tasks
@Parameter(title: "Show completed", default: true)
var showCompleted: Bool
// Maximum number of tasks
@Parameter(title: "Task count", default: 3)
var maxTasks: Int
}
// Enum for priority filter
enum PriorityFilter: String, AppEnum {
case all
case high
case medium
case low
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Priority"
static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
.all: "All",
.high: "High",
.medium: "Medium",
.low: "Low"
]
}
// Provider adapted to configuration
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))
}
// Apply configuration filters
private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
var tasks = TaskDataManager.shared.fetchTasks()
// Filter by priority
if config.priorityFilter != .all {
let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
tasks = tasks.filter { /bin/bash.priority == priority }
}
// Filter completed if needed
if !config.showCompleted {
tasks = tasks.filter { !/bin/bash.isCompleted }
}
// Limit count
return Array(tasks.prefix(config.maxTasks))
}
}
// Widget with user configuration
struct ConfigurableTaskWidget: Widget {
let kind: String = "ConfigurableTaskWidget"
var body: some WidgetConfiguration {
// AppIntentConfiguration for configurable widgets
AppIntentConfiguration(
kind: kind,
intent: TaskWidgetConfigurationIntent.self,
provider: ConfigurableTaskProvider()
) { entry in
TaskWidgetView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Custom Tasks")
.description("Filter and customize your tasks.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}O usuário agora pode configurar o widget com um toque longo, oferecendo uma experiência personalizada sem código adicional na aplicação. Para mais informações sobre App Intents e integração com Siri, consulte App Intents 2.0 and Siri Shortcuts.
Melhores Práticas e Otimizações
Vários padrões garantem widgets interativos eficientes e confiáveis.
import WidgetKit
import SwiftUI
// 1. Always invalidate cache after modification
final class WidgetRefreshManager {
static func refreshAllWidgets() {
// Refresh all app widgets
WidgetCenter.shared.reloadAllTimelines()
}
static func refreshWidget(kind: String) {
// Refresh specific widget
WidgetCenter.shared.reloadTimelines(ofKind: kind)
}
// Call from app after data modification
static func notifyDataChanged() {
Task { @MainActor in
refreshAllWidgets()
}
}
}
// 2. Limit view complexity
struct OptimizedWidgetView: View {
let entry: TaskEntry
var body: some View {
// Prefer simple views without GeometryReader
VStack(alignment: .leading, spacing: 8) {
ForEach(entry.tasks.prefix(3)) { task in
// Lightweight components
minimalTaskRow(task)
}
}
.padding()
}
// Minimal view for 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. Use @AppStorage for simple state
struct QuickSettingsWidgetView: View {
// Direct access to shared UserDefaults
@AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
private var showCompleted = true
var body: some View {
// State persists between refreshes
Text(showCompleted ? "Showing all" : "Hiding completed")
}
}
// 4. Pre-calculate data in provider
struct OptimizedTaskEntry: TimelineEntry {
let date: Date
let tasks: [Task]
// Pre-calculated data
let completedCount: Int
let pendingCount: Int
let highPriorityCount: Int
init(date: Date, tasks: [Task]) {
self.date = date
self.tasks = tasks
// Calculations performed once
self.completedCount = tasks.filter(.isCompleted).count
self.pendingCount = tasks.filter { !/bin/bash.isCompleted }.count
self.highPriorityCount = tasks.filter { /bin/bash.priority == .high && !/bin/bash.isCompleted }.count
}
}Essas otimizações garantem widgets responsivos que não consomem excessivamente a bateria.
Usar o scheme widget no Xcode para depuração. O preview canvas permite testar os diferentes tamanhos e estados sem instalação no dispositivo.
Fontes
- WidgetKit Documentation - Referência oficial do WidgetKit da Apple
- App Intents Framework - Documentação completa da API de App Intents
- WWDC 2026: Discover new capabilities in the App Intents framework - EntityCollection, LongRunningIntent, SyncableEntity
- Human Interface Guidelines: Materials - Guia de design Liquid Glass
Checklist de Desenvolvimento de Widgets Interativos iOS
O WidgetKit com App Intents transforma os widgets em verdadeiras extensões interativas das aplicações iOS. A arquitetura introduzida no iOS 17 permanece estável até o iOS 26, com refinamentos visuais e funcionalidades de performance adicionados ao longo do caminho.
Checklist de implementação
- Configurar App Group para compartilhamento de dados entre o app e a extensão do widget
- Criar Timeline Provider com política de atualização apropriada
- Implementar App Intents para cada ação do usuário
- Usar
Button(intent:)ouToggle(intent:)para interatividade - Chamar
WidgetCenter.shared.reloadTimelinesapós modificação de dados - Adicionar o
.containerBackgroundobrigatório para compatibilidade iOS 17+ - Usar
.widgetAccentedRenderingModepara suporte ao Liquid Glass no iOS 26 - Implementar animações de transição suaves com
.contentTransition - Gerenciar estados de carregamento e erro para feedback visual
- Usar
EntityCollectionpara operações em lote evitando overhead de resolução - Testar em todos os tamanhos de widget compatíveis incluindo
.systemExtraLarge
Comece a praticar!
Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.
Você saberia encontrar o bug em iOS?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador da SharpSkill
Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.
Atualizado em 24 de agosto de 2026
Tags
Compartilhar
Artigos relacionados

App Intents 2.0 e Siri Shortcuts: guia de automação iOS 27
Guia completo de App Intents 2.0 e Siri Shortcuts para iOS 27. Criar streaming responses, conversas multi-turno, View Annotations e integrar com Foundation Models.

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.