WidgetKit permite añadir widgets a la pantalla de inicio y a la pantalla de bloqueo de iOS. A diferencia de las extensiones de hoy en día, los widgets no son vistas interactivas en tiempo real, sino instantáneas programadas que el sistema renderiza y muestra cuando lo necesita. Entender este modelo es clave para implementar widgets correctamente.
La estructura básica: Widget, TimelineProvider y TimelineEntry
Un widget se compone de tres piezas: la entrada con los datos en un momento dado (TimelineEntry), el proveedor que decide qué entradas generar y cuándo (TimelineProvider), y la vista que renderiza cada entrada.
import WidgetKit
import SwiftUI
// 1. Entrada: los datos de una snapshot concreta
struct EntradaClima: TimelineEntry {
let date: Date
let temperatura: Double
let ciudad: String
let icono: String
}
// 2. Proveedor: genera las entradas del timeline
struct ProveedorClima: TimelineProvider {
// Vista placeholder mientras se cargan datos reales
func placeholder(in context: Context) -> EntradaClima {
EntradaClima(date: Date(), temperatura: 22.0, ciudad: "Madrid", icono: "sun.max")
}
// Snapshot para la galería de widgets
func getSnapshot(in context: Context, completion: @escaping (EntradaClima) -> Void) {
let entrada = EntradaClima(date: Date(), temperatura: 22.0, ciudad: "Madrid", icono: "sun.max")
completion(entrada)
}
// Timeline completo: array de entradas y política de recarga
func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
Task {
let clima = await obtenerClima()
let ahora = Date()
let entradas = (0..<12).map { hora in
EntradaClima(
date: ahora.addingTimeInterval(Double(hora) * 3600),
temperatura: clima.temperatura,
ciudad: clima.ciudad,
icono: clima.icono
)
}
let timeline = Timeline(entries: entradas, policy: .atEnd)
completion(timeline)
}
}
}
// 3. Vista del widget
struct VistaClima: View {
var entry: EntradaClima
var body: some View {
VStack {
Image(systemName: entry.icono)
.font(.largeTitle)
Text("(entry.temperatura, format: .number.precision(.fractionLength(1)))°")
.font(.title2.bold())
Text(entry.ciudad)
.font(.caption)
}
.containerBackground(.blue.gradient, for: .widget)
}
}
// 4. Definición del widget
struct ClimaWidget: Widget {
let kind: String = "ClimaWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: ProveedorClima()) { entry in
VistaClima(entry: entry)
}
.configurationDisplayName("Clima")
.description("Muestra la temperatura actual.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
TimelineReloadPolicy: cuándo recargar
La política de recarga determina cuándo el sistema pedirá un nuevo timeline:
// Recargar cuando se consuman todas las entradas del timeline
let timeline = Timeline(entries: entradas, policy: .atEnd)
// Recargar en una fecha específica, independientemente de las entradas
let maniana = Calendar.current.date(byAdding: .day, value: 1, to: Date())!
let timeline = Timeline(entries: entradas, policy: .after(maniana))
// No recargar hasta que la app lo solicite explícitamente
let timeline = Timeline(entries: entradas, policy: .never)
Para recargar desde la app principal:
import WidgetKit
// Recargar todos los widgets
WidgetCenter.shared.reloadAllTimelines()
// Recargar solo un tipo específico
WidgetCenter.shared.reloadTimelines(ofKind: "ClimaWidget")
App Group: compartir datos entre app y widget
El widget se ejecuta en un proceso separado y no puede acceder directamente a los datos de la app principal. La solución es un App Group compartido:
// En la app principal: guardar datos en el grupo compartido
let userDefaults = UserDefaults(suiteName: "group.com.miempresa.miapp")
userDefaults?.set(temperatura, forKey: "temperatura_actual")
userDefaults?.set(ciudad, forKey: "ciudad_actual")
// En el widget: leer los mismos datos
func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
let ud = UserDefaults(suiteName: "group.com.miempresa.miapp")
let temperatura = ud?.double(forKey: "temperatura_actual") ?? 0
let ciudad = ud?.string(forKey: "ciudad_actual") ?? "Desconocida"
let entrada = EntradaClima(date: Date(), temperatura: temperatura, ciudad: ciudad, icono: "cloud")
let timeline = Timeline(entries: [entrada], policy: .after(Date().addingTimeInterval(3600)))
completion(timeline)
}
Deep linking con widgetURL y Link
Para que el widget abra la app en un contexto específico al tocarlo, usa widgetURL (para systemSmall) o Link (para tamaños que pueden tener múltiples zonas tocables):
// Widget pequeño: una sola URL de destino
struct VistaTarea: View {
var entry: EntradaTarea
var body: some View {
VStack(alignment: .leading) {
Text(entry.titulo)
Text(entry.fecha, style: .relative)
.font(.caption)
}
.widgetURL(URL(string: "miapp://tarea/(entry.id)")!)
.containerBackground(.white, for: .widget)
}
}
// Widget mediano: varias zonas tocables independientes
struct VistaListaTareas: View {
var entry: EntradaListaTareas
var body: some View {
VStack(alignment: .leading, spacing: 4) {
ForEach(entry.tareas.prefix(3)) { tarea in
Link(destination: URL(string: "miapp://tarea/(tarea.id)")!) {
Text(tarea.titulo).lineLimit(1)
}
}
}
.containerBackground(.white, for: .widget)
}
}
Relevance en Smart Stacks
Para que el sistema muestre tu widget en el momento más apropiado dentro de una Smart Stack, proporciona información de relevancia:
struct EntradaReunion: TimelineEntry {
let date: Date
let reunion: Reunion
var relevance: TimelineEntryRelevance? {
// Relevancia alta cuando la reunión empieza en menos de 30 min
let minutosHastaReunion = reunion.inicio.timeIntervalSinceNow / 60
if minutosHastaReunion < 30 && minutosHastaReunion > 0 {
return TimelineEntryRelevance(score: 1.0, duration: 30 * 60)
}
return TimelineEntryRelevance(score: 0.3)
}
}
Resumen
WidgetKit sigue un modelo de snapshots programadas, no de vistas en tiempo real. El TimelineProvider genera entradas con datos y fechas; la política de recarga controla cuándo el sistema pide nuevos datos; los App Groups sincronizan datos entre la app y el widget; y widgetURL o Link conectan los widgets con rutas concretas de la app. Entender este modelo de datos inmutables y actualizaciones discretas es el primer paso para crear widgets que el sistema gestione de forma eficiente.
