OdoKeep ya está en el App Store. Descárgalo gratis.

Todos los artículos

Widgets iOS

Widgets en React Native sin duplicar la lógica de negocio en Swift

Una instantánea JSON versionada permite a WidgetKit reutilizar cálculos TypeScript, avanzar vencimientos y respetar la privacidad.

Actualizado el

En este artículo

«Próximo mantenimiento» es una etiqueta corta con muchas dependencias.

En OdoKeep puede depender del vehículo, su mantenimiento registrado, la unidad de distancia, los aplazamientos del usuario y la previsión actual de servicio. El gasto mensual tiene otras reglas, como convertir un registro en moneda extranjera según su propia fecha.

Esas respuestas ya existían en TypeScript. Añadir widgets de iOS era una buena oportunidad para copiarlas en Swift y pasar el año siguiente arreglando desacuerdos entre ambas versiones.

En lugar de eso, la aplicación escribe las respuestas.

OdoKeep usa React Native y Expo. Tiene cuatro widgets de WidgetKit: detalles del vehículo, vencimientos, gasto mensual y mantenimiento. La extensión es un proceso independiente. No ejecuta React Native ni abre el almacenamiento MMKV de la aplicación.

Los dos procesos se comunican mediante una instantánea JSON en un contenedor App Group. TypeScript la construye con los mismos módulos del panel. Swift la decodifica y la muestra.

Compartir respuestas terminadas entre procesos

La instantánea es un contrato de presentación, no una exportación de registros sin procesar. Incluye importes formateados, unidades aplicadas, etiquetas traducidas y enlaces profundos construidos. Incluso las barras del gráfico de gasto llevan sus etiquetas y proporciones normalizadas.

Este extracto muestra la forma del contrato:

interface WidgetSpendBucket {
  id: string;
  label: string;
  ratio: number;
  isCurrent: boolean;
}

interface WidgetDeadline {
  id: string;
  label: string;
  vehicleId: string;
  vehicleName: string;
  dueDate: string;
  dueDateLabel: string;
  warningFrom: string;
  url: string;
}

El archivo conserva estructura útil. Swift necesita identidades para seleccionar vehículos, fechas para avanzar la vista y proporciones para dibujar barras. No necesita otra implementación de las reglas financieras o de mantenimiento.

La lista de avisos del panel, la previsión de servicio y los cálculos de gasto alimentan lib/widgets/snapshot.ts. plugins/widgets/WidgetsSnapshot.swift refleja el contrato del decodificador. Si cambia el significado de un campo, cambia también la versión compartida. Un decodificador que no entiende esa versión pide abrir la aplicación.

Es un fallo mucho más fácil de investigar que un widget que muestra con seguridad una interpretación equivocada.

Dejar que avance el calendario

El calendario es la excepción deliberada a las respuestas precalculadas. Un widget puede seguir visible durante días sin abrir la aplicación. «Dentro de tres días» debe cambiar después de medianoche sin pedir a React Native que lo reconstruya.

Por eso la instantánea guarda el vencimiento como un día del calendario local, otro día warningFrom y textos traducidos indexados por desplazamiento:

"0"  -> Hoy
"1"  -> Mañana
"2"  -> Dentro de 2 días
"-2" -> Venció hace 2 días

La tabla real usa la función de traducción de la aplicación, cubre sesenta días pasados y 120 futuros e incluye una alternativa para vencimientos anteriores. Swift cuenta días del calendario y busca el texto terminado. Puede activar el aviso en la fecha calculada sin saber cómo se decidió esa fecha.

Para mantenimientos por distancia, la etiqueta es fija. Pasar una noche no añade kilómetros al odómetro.

WidgetTimeline.swift crea una entrada para ahora y otra por cada medianoche local de la siguiente semana, con política .atEnd. La instantánea no cambia; cambia la fecha de cada entrada. Se proporcionan estados futuros a WidgetKit, sin prometer una ejecución en un instante exacto. WidgetKit controla la programación, como explica Apple en su documentación sobre actualización de widgets.

Sustituir el archivo antes de pedir la recarga

La escritura principal ocurre cuando la aplicación deja el primer plano, cerca del momento en que el usuario vuelve a la pantalla de inicio. Un temporizador de cuatro segundos tras el montaje genera la instantánea inicial de una sesión que todavía no ha pasado a segundo plano. Los cambios observables de preferencias y configuración vuelven a programarlo.

El repositorio no expone una suscripción para cada escritura de registros. Por eso, un registro añadido después de dispararse el temporizador llega al widget al pasar después a segundo plano. Ese es el límite de actualización. Tampoco puede mostrarse una edición compartida que la aplicación aún no haya descargado.

El escritor usa operaciones síncronas en esa transición. Escribe el JSON entero con un nombre temporal y después lo mueve sobre la instantánea real permitiendo sobrescritura. Así el decodificador habitual no encuentra un documento escrito a medias. Un archivo temporal de un intento interrumpido se elimina antes de reutilizarlo.

Solo después de la sustitución, un pequeño módulo nativo Expo solicita recargar las cronologías de WidgetKit. Escribir bytes no cambia la cronología actual hasta que el sistema pide otra. La recarga es una petición al planificador; la actualización depende de iOS y de una escritura correcta.

Aplicar la privacidad al construir la instantánea

La extensión recibe menos información de la que tiene la aplicación. Con el bloqueo biométrico activado, no se incluyen valores de odómetro ni dinero. Esos campos son nulos; los datos sensibles no se serializan para ocultarlos después. La matrícula nunca se incluye, con o sin bloqueo. Los vencimientos y el mantenimiento siguen disponibles.

Las vistas Swift también marcan valores sensibles para que el sistema pueda ocultarlos. Eso respalda los controles de visualización de iOS, mientras el constructor decide qué datos llegan siquiera a la extensión.

Cerrar sesión o eliminar la cuenta borra la instantánea y solicita una recarga. Borrar el archivo forma parte del cierre de sesión, no solo del desmontaje del hook. De otro modo, podría sobrevivir a la cuenta que lo produjo.

Reutilizar la separación con Siri y Atajos

Los intents de lectura de Siri y Atajos usan otra instantánea de presentación, almacenada en Documents de la aplicación. Compilan en el target principal, no en la extensión WidgetKit. Los constructores comparten entradas, pero sus archivos y consumidores tienen contratos distintos.

Un atajo de escritura abre un enlace profundo al formulario existente. No modifica almacenamiento desde Swift. La ruta sigue aplicando permisos de colaboración, límites del plan, comprobaciones del odómetro y sincronización. Los parámetros de Siri rellenan una solicitud; no evitan la ruta normal de escritura.

Reproducir la extensión con Expo prebuild

El proyecto regenera su directorio iOS, ignorado por Git, mediante Expo prebuild. Un target añadido a mano en Xcode no sobreviviría. Por eso plugins/with-widgets.js crea y configura la extensión, enlaza Swift y recursos, declara App Group y describe el segundo target a EAS para la firma. Sigue el modelo de config plugins de Expo para cambios que deben sobrevivir a la regeneración nativa.

Una opción merece explicación: ENABLE_DEBUG_DYLIB = NO en la extensión. La implementación documenta un fallo por el que la extracción de metadatos App Intents inspeccionaba dependencias del ejecutable auxiliar de depuración, no encontraba AppIntents y omitía la extracción sin hacer fallar la compilación. Los widgets configurables se quedaban sin intent utilizable y mostraban un estado vacío aunque hubiera sesión iniciada. Esa opción proporciona la disposición binaria esperada por la extracción.

Las pruebas comprueban el constructor puro, los campos exigidos por Swift, enlaces profundos, tablas de traducción, colores, fuentes y configuración nativa. Son comprobaciones concretas del contrato, no un esquema generado entre lenguajes ni un sustituto de probar en un dispositivo. Detectan divergencias entre consumidores compilados por separado antes de llegar a la pantalla de inicio.

Reutilizaría esta separación en otro producto React Native: la aplicación decide el significado de los datos y envía una descripción acotada y versionada de lo que puede mostrarse. Las fechas conservan estructura suficiente para avanzar; las demás respuestas envejecen de forma explícita desde la última escritura.

Estos cuatro widgets están en OdoKeep para iOS. Permiten consultar vencimientos y el siguiente mantenimiento sin abrir todo el diario.