WidgetKit iOS 17+: Widgets Interactivos con App Intents

Guía completa para crear widgets iOS interactivos con WidgetKit y App Intents. Botones, toggles, animaciones y mejores prácticas para iOS 17+ en 2026.

WidgetKit iOS 17+ con widgets interactivos y App Intents para aplicaciones iOS modernas

iOS 17 revolucionó WidgetKit al introducir interactividad nativa. Los widgets ya no son pantallas estáticas: ahora pueden responder a las acciones del usuario directamente desde la pantalla de inicio, sin abrir la aplicación. Esta evolución mayor se apoya en el framework App Intents y ofrece una experiencia de usuario fluida y moderna.

Lo que cubre este artículo

Este artículo presenta la creación completa de widgets interactivos iOS 17+, desde la configuración del proyecto hasta los patrones avanzados con animaciones y gestión de estado.

Arquitectura de los Widgets Interactivos

La interactividad de los widgets iOS 17+ funciona mediante el framework App Intents. A diferencia de los deep links tradicionales que abrirían la aplicación, los App Intents permiten ejecutar código directamente desde el widget, refrescando luego automáticamente la visualización con los nuevos datos.

InteractiveWidgetArchitecture.swiftswift
import WidgetKit
import SwiftUI
import AppIntents

// La arquitectura se basa en tres componentes principales:
// 1. Widget Timeline Provider - proporciona los datos
// 2. Widget View - muestra la interfaz con Button/Toggle
// 3. App Intent - ejecuta la acción al pulsar

struct TaskWidget: Widget {
    // Identificador único del widget
    let kind: String = "TaskWidget"

    var body: some WidgetConfiguration {
        // StaticConfiguration para widgets sin parámetros
        StaticConfiguration(
            kind: kind,
            provider: TaskTimelineProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                // Obligatorio para los App Intents
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Tareas")
        .description("Gestiona tus tareas desde la pantalla de inicio.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

El widget declara su configuración y especifica el provider que suministrará los datos. El atributo .containerBackground es obligatorio desde iOS 17 para los widgets interactivos.

Creación del Timeline Provider

El Timeline Provider determina cuándo y cómo se refresca el widget. Para los widgets interactivos, también debe reaccionar a los cambios provocados por los App Intents.

TaskTimelineProvider.swiftswift
import WidgetKit
import SwiftUI

// Entry que representa el estado del widget en un momento dado
struct TaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // Estado de carga para el feedback visual
    var isLoading: Bool = false
}

// Modelo de datos compartido entre app y 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 mostrado durante la carga inicial
    func placeholder(in context: Context) -> TaskEntry {
        TaskEntry(
            date: Date(),
            tasks: [
                Task(id: UUID(), title: "Tarea de ejemplo", isCompleted: false, priority: .medium)
            ]
        )
    }

    // Snapshot para la galería 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 con política de refresco
    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)))

        // Refresco en 15 minutos o tras una acción del usuario
        let nextUpdate = Calendar.current.date(
            byAdding: .minute,
            value: 15,
            to: Date()
        ) ?? Date()

        let timeline = Timeline(
            entries: [entry],
            policy: .after(nextUpdate)
        )
        completion(timeline)
    }
}

El provider utiliza un TaskDataManager compartido para acceder a los datos. Este enfoque garantiza la sincronización entre la aplicación principal y el widget.

App Group obligatorio

Para compartir datos entre la app y el widget, hay que configurar un App Group en las capabilities del proyecto. Los UserDefaults o archivos deben usar este grupo compartido.

El Gestor de Datos Compartidos

El intercambio de datos entre la aplicación y el widget requiere un contenedor común accesible mediante App Group.

TaskDataManager.swiftswift
import Foundation

final class TaskDataManager {
    // Singleton para acceso global
    static let shared = TaskDataManager()

    // Identificador del App Group configurado en Xcode
    private let appGroupID = "group.com.example.taskapp"

    // UserDefaults compartido entre app y widget
    private var sharedDefaults: UserDefaults? {
        UserDefaults(suiteName: appGroupID)
    }

    private let tasksKey = "tasks"

    private init() {}

    // Recupera las tareas del almacenamiento compartido
    func fetchTasks() -> [Task] {
        guard let data = sharedDefaults?.data(forKey: tasksKey),
              let tasks = try? JSONDecoder().decode([Task].self, from: data) else {
            return []
        }
        return tasks
    }

    // Guarda con notificación al widget
    func saveTasks(_ tasks: [Task]) {
        guard let data = try? JSONEncoder().encode(tasks) else { return }
        sharedDefaults?.set(data, forKey: tasksKey)
    }

    // Actualiza una tarea específica
    func updateTask(_ task: Task) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.id == task.id }) {
            tasks[index] = task
            saveTasks(tasks)
        }
    }

    // Cambia el estado de completado
    func toggleTaskCompletion(taskID: UUID) {
        var tasks = fetchTasks()
        if let index = tasks.firstIndex(where: { $0.id == taskID }) {
            tasks[index].isCompleted.toggle()
            saveTasks(tasks)
        }
    }
}

Este gestor encapsula toda la lógica de persistencia y será utilizado tanto por la aplicación como por los App Intents del widget.

Creación del App Intent para la Interactividad

El App Intent define la acción ejecutada cuando el usuario interactúa con el widget. iOS ejecuta esta acción en segundo plano y luego refresca el widget automáticamente.

ToggleTaskIntent.swiftswift
import AppIntents
import WidgetKit

// Intent para alternar el estado de una tarea
struct ToggleTaskIntent: AppIntent {
    // Título mostrado en los atajos de Siri
    static var title: LocalizedStringResource = "Alternar estado de la tarea"

    // Descripción para accesibilidad
    static var description = IntentDescription("Marca una tarea como completada o no completada.")

    // Parámetro: ID de la tarea a modificar
    @Parameter(title: "ID de la tarea")
    var taskID: String

    // Inicializador requerido por AppIntent
    init() {}

    // Inicializador con parámetro para creación desde la vista
    init(taskID: UUID) {
        self.taskID = taskID.uuidString
    }

    // Ejecución de la acción
    func perform() async throws -> some IntentResult {
        // Conversión del ID string a UUID
        guard let uuid = UUID(uuidString: taskID) else {
            return .result()
        }

        // Actualización de la tarea
        TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)

        // Solicita el refresco del widget
        WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")

        return .result()
    }
}

La llamada a WidgetCenter.shared.reloadTimelines provoca un refresco inmediato del widget tras la acción, asegurando un feedback visual instantáneo.

¿Listo para aprobar tus entrevistas de iOS?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Vista del Widget con Botones Interactivos

La vista del widget utiliza el componente Button estándar de SwiftUI con el intent como acción. iOS 17+ intercepta automáticamente estas interacciones para ejecutar el App Intent.

TaskWidgetView.swiftswift
import SwiftUI
import WidgetKit

struct TaskWidgetView: View {
    let entry: TaskEntry

    // Adaptación al tamaño del widget
    @Environment(\.widgetFamily) var family

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            // Encabezado con título y contador
            headerView

            // Lista de tareas con botones interactivos
            ForEach(entry.tasks.prefix(tasksLimit)) { task in
                TaskRowView(task: task)
            }

            Spacer(minLength: 0)
        }
        .padding()
    }

    // Número de tareas según el tamaño
    private var tasksLimit: Int {
        switch family {
        case .systemSmall: return 2
        case .systemMedium: return 3
        default: return 4
        }
    }

    private var headerView: some View {
        HStack {
            Text("Tareas")
                .font(.headline)
                .fontWeight(.bold)

            Spacer()

            // Insignia con el número de tareas 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ón con App Intent como acción
        Button(intent: ToggleTaskIntent(taskID: task.id)) {
            HStack(spacing: 12) {
                // Indicador de completado
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)

                // Título de la tarea
                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    .lineLimit(1)

                Spacer()

                // Indicador de prioridad
                priorityIndicator
            }
            .padding(.vertical, 6)
            .padding(.horizontal, 10)
            .background(Color(.systemBackground).opacity(0.5))
            .cornerRadius(8)
        }
        .buttonStyle(.plain)
    }

    @ViewBuilder
    private var priorityIndicator: some View {
        switch task.priority {
        case .high:
            Image(systemName: "exclamationmark.circle.fill")
                .foregroundStyle(.red)
        case .medium:
            Image(systemName: "minus.circle.fill")
                .foregroundStyle(.orange)
        case .low:
            EmptyView()
        }
    }
}

La sintaxis Button(intent:) conecta directamente el botón con el App Intent. Al pulsarlo, iOS ejecuta perform() y luego refresca automáticamente el widget.

Toggle Interactivo para los Widgets

Para las acciones de tipo encendido/apagado, el componente Toggle ofrece una alternativa al botón con un estilo nativo de iOS.

ToggleWidgetView.swiftswift
import SwiftUI
import AppIntents

// Intent específico para Toggle con estado explícito
struct SetTaskCompletionIntent: AppIntent {
    static var title: LocalizedStringResource = "Definir estado de la tarea"

    @Parameter(title: "ID de la tarea")
    var taskID: String

    // Estado deseado: true = completada, false = no completada
    @Parameter(title: "Completada")
    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 el estado de manera explícita (no 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 interactivo con intent
            Toggle(
                isOn: task.isCompleted,
                intent: SetTaskCompletionIntent(
                    taskID: task.id,
                    isCompleted: !task.isCompleted
                )
            )
            .toggleStyle(.switch)
            .labelsHidden()
        }
        .padding(.vertical, 4)
    }
}

Toggle ofrece una interacción más intuitiva para los estados binarios y se integra naturalmente en los diseños iOS.

Limitaciones de los widgets interactivos

Los widgets no pueden mostrar alertas, sheets ni navegación. Todas las acciones deben ser autónomas y actualizar directamente el estado visible.

Animaciones de Refresco y Transiciones

iOS 17+ permite animar las transiciones durante el refresco del widget tras una acción. El modificador .contentTransition controla estas animaciones.

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) {
                // Icono con animación de transición
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .font(.title3)
                    .foregroundStyle(task.isCompleted ? .green : .secondary)
                    // Animación del icono al cambiar
                    .contentTransition(.symbolEffect(.replace))

                Text(task.title)
                    .font(.subheadline)
                    .strikethrough(task.isCompleted)
                    .foregroundStyle(task.isCompleted ? .secondary : .primary)
                    // Animación del texto
                    .contentTransition(.opacity)

                Spacer()
            }
            .padding(.vertical, 6)
            .padding(.horizontal, 10)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(task.isCompleted ? Color.green.opacity(0.1) : Color.clear)
            )
            // Animación del fondo
            .animation(.easeInOut(duration: 0.3), value: task.isCompleted)
        }
        .buttonStyle(.plain)
    }
}

// Widget con invalidación 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("Tareas Animadas")
        .description("Widgets con animaciones suaves.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
        // Activación de las animaciones de contenido
        .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("Tareas")
                .font(.headline.bold())

            Spacer()

            let completed = entry.tasks.filter(\.isCompleted).count
            let total = entry.tasks.count

            // Progreso animado
            Text("\(completed)/\(total)")
                .font(.caption.bold())
                .foregroundStyle(.secondary)
                .contentTransition(.numericText())
        }
    }
}

Las animaciones .symbolEffect(.replace) y .numericText() crean transiciones suaves entre estados, mejorando significativamente la experiencia del usuario.

Widget Configurable con AppIntentConfiguration

Para los widgets personalizables por el usuario (filtros, categorías), AppIntentConfiguration sustituye a StaticConfiguration.

ConfigurableTaskWidget.swiftswift
import WidgetKit
import SwiftUI
import AppIntents

// Configuración expuesta al usuario
struct TaskWidgetConfigurationIntent: WidgetConfigurationIntent {
    static var title: LocalizedStringResource = "Configuración de tareas"
    static var description = IntentDescription("Personaliza la visualización de tareas.")

    // Filtro por prioridad
    @Parameter(title: "Prioridad", default: .all)
    var priorityFilter: PriorityFilter

    // Mostrar tareas completadas
    @Parameter(title: "Mostrar completadas", default: true)
    var showCompleted: Bool

    // Número máximo de tareas
    @Parameter(title: "Número de tareas", default: 3)
    var maxTasks: Int
}

// Enum para el filtro de prioridad
enum PriorityFilter: String, AppEnum {
    case all
    case high
    case medium
    case low

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Prioridad"

    static var caseDisplayRepresentations: [PriorityFilter: DisplayRepresentation] = [
        .all: "Todas",
        .high: "Alta",
        .medium: "Media",
        .low: "Baja"
    ]
}

// Provider adaptado a la configuración
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 los filtros de configuración
    private func filteredTasks(for config: TaskWidgetConfigurationIntent) -> [Task] {
        var tasks = TaskDataManager.shared.fetchTasks()

        // Filtrado por prioridad
        if config.priorityFilter != .all {
            let priority = Task.Priority(rawValue: config.priorityFilter.rawValue) ?? .medium
            tasks = tasks.filter { $0.priority == priority }
        }

        // Filtrado de completadas si es necesario
        if !config.showCompleted {
            tasks = tasks.filter { !$0.isCompleted }
        }

        // Limitación del número
        return Array(tasks.prefix(config.maxTasks))
    }
}

// Widget con configuración del usuario
struct ConfigurableTaskWidget: Widget {
    let kind: String = "ConfigurableTaskWidget"

    var body: some WidgetConfiguration {
        // AppIntentConfiguration para los widgets configurables
        AppIntentConfiguration(
            kind: kind,
            intent: TaskWidgetConfigurationIntent.self,
            provider: ConfigurableTaskProvider()
        ) { entry in
            TaskWidgetView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Tareas Personalizadas")
        .description("Filtra y personaliza tus tareas.")
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

El usuario ahora puede configurar el widget mediante una pulsación larga, ofreciendo una experiencia personalizada sin código adicional en la aplicación.

Gestión de Errores y Estados de Carga

Una buena UX requiere gestionar los casos de error y los estados intermedios durante las interacciones.

TaskIntentWithFeedback.swiftswift
import AppIntents
import WidgetKit

struct ToggleTaskWithFeedbackIntent: AppIntent {
    static var title: LocalizedStringResource = "Alternar tarea con feedback"

    @Parameter(title: "ID de la tarea")
    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 {
            // Devuelve un fallo silencioso
            return .result(value: false)
        }

        // Simula una operación asíncrona (sincronización con servidor por ejemplo)
        do {
            try await Task.sleep(for: .milliseconds(100))

            TaskDataManager.shared.toggleTaskCompletion(taskID: uuid)
            WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")

            return .result(value: true)
        } catch {
            // Error: no actualizar el widget
            return .result(value: false)
        }
    }
}

// Vista con estado de carga
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)
    }
}

Un feedback visual inmediato (opacidad reducida, indicador de carga) informa al usuario de que su acción ha sido registrada.

Mejores Prácticas y Optimizaciones

Varios patrones aseguran widgets interactivos eficientes y fiables.

WidgetBestPractices.swiftswift
import WidgetKit
import SwiftUI

// 1. Invalidar siempre la caché tras una modificación
final class WidgetRefreshManager {
    static func refreshAllWidgets() {
        // Refresco de todos los widgets de la app
        WidgetCenter.shared.reloadAllTimelines()
    }

    static func refreshWidget(kind: String) {
        // Refresco de un widget específico
        WidgetCenter.shared.reloadTimelines(ofKind: kind)
    }

    // Llamada desde la app tras una modificación de datos
    static func notifyDataChanged() {
        Task { @MainActor in
            refreshAllWidgets()
        }
    }
}

// 2. Limitar la complejidad de las vistas
struct OptimizedWidgetView: View {
    let entry: TaskEntry

    var body: some View {
        // Preferir vistas simples sin GeometryReader
        VStack(alignment: .leading, spacing: 8) {
            ForEach(entry.tasks.prefix(3)) { task in
                // Componentes ligeros
                minimalTaskRow(task)
            }
        }
        .padding()
    }

    // Vista mínima para el rendimiento
    @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 los estados simples
struct QuickSettingsWidgetView: View {
    // Acceso directo a los UserDefaults compartidos
    @AppStorage("showCompletedTasks", store: UserDefaults(suiteName: "group.com.example.app"))
    private var showCompleted = true

    var body: some View {
        // El estado persiste entre refrescos
        Text(showCompleted ? "Mostrando todas" : "Ocultando completadas")
    }
}

// 4. Pre-calcular los datos en el provider
struct OptimizedTaskEntry: TimelineEntry {
    let date: Date
    let tasks: [Task]

    // Datos pre-calculados
    let completedCount: Int
    let pendingCount: Int
    let highPriorityCount: Int

    init(date: Date, tasks: [Task]) {
        self.date = date
        self.tasks = tasks

        // Cálculos efectuados una sola 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 optimizaciones garantizan widgets reactivos que no consumen excesivamente la batería.

Depuración de los widgets

Usar el scheme widget en Xcode para la depuración. La preview canvas permite probar los diferentes tamaños y estados sin instalación en el dispositivo.

Conclusión

WidgetKit iOS 17+ con App Intents transforma los widgets en verdaderas extensiones interactivas de las aplicaciones iOS. Esta arquitectura declarativa simplifica considerablemente el desarrollo ofreciendo una experiencia de usuario nativa y fluida.

Checklist Widget Interactivo iOS 17+

  • ✅ Configurar un App Group para compartir datos
  • ✅ Crear un Timeline Provider con refresco apropiado
  • ✅ Implementar App Intents para cada acción
  • ✅ Usar Button(intent:) o Toggle(intent:) para la interactividad
  • ✅ Llamar a WidgetCenter.shared.reloadTimelines tras modificación
  • ✅ Añadir el .containerBackground obligatorio para iOS 17+
  • ✅ Implementar animaciones de transición suaves
  • ✅ Gestionar los estados de carga y error
  • ✅ Optimizar las vistas para el rendimiento de la batería
  • ✅ Probar en todos los tamaños de widget compatibles

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Etiquetas

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

Compartir

Artículos relacionados