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.

WidgetKit iOS 17+ com widgets interativos e App Intents para aplicações iOS modernas

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.

InteractiveWidgetArchitecture.swiftswift
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.

TaskTimelineProvider.swiftswift
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.

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.

TaskDataManager.swiftswift
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.

ToggleTaskIntent.swiftswift
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.

TaskWidgetView.swiftswift
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.

ToggleWidgetView.swiftswift
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.

AnimatedTaskWidgetView.swiftswift
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.

ConfigurableTaskWidget.swiftswift
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.

TaskIntentWithFeedback.swiftswift
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.

WidgetBestPractices.swiftswift
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

Comece a praticar!

Teste seus conhecimentos com nossos simuladores de entrevista e testes tecnicos.

Tags

#widgetkit
#ios
#app-intents
#swift
#widgets

Compartilhar

Artigos relacionados