Todos los artículos
9 de octubre de 2026

Apps iOS offline-first: sincronización, resolución de conflictos y segundo plano

Cómo crear una app iOS offline first: almacén local como fuente de verdad, sincronización con una cola de salida, regla de conflictos y tareas en segundo plano.

Manos escribiendo en un teclado inalámbrico blanco sobre un escritorio rojo para una app iOS offline-first

Una app iOS offline-first lee y escribe en una base de datos local y trata la red como una tarea en segundo plano. Para construirla necesitas cuatro piezas: un almacén local que sea la fuente de verdad, una cola de salida con los cambios pendientes, una regla de conflictos explícita y disparadores en segundo plano en los que nunca confíes por sí solos.

La mayoría de las apps fallan en la última. iOS no promete ejecutar tu código de sincronización. El diseño tiene que funcionar incluso cuando no lo ejecuta nunca. Esta guía recorre cada pieza en el orden en que la construirías.

Empieza con un almacén local como fuente de verdad

Cada pantalla debería leer del almacén local y cada acción del usuario debería escribir primero en él. La sesión de Apple de WWDC19 sobre Core Data con CloudKit lo describe como una réplica local de los datos y da la razón: las lecturas locales tardan milisegundos en el peor caso, mientras que las consultas de red pueden tardar segundos o minutos. El usuario nunca espera a una petición para ver sus propios datos.

Elige primero el almacén

El almacén que elijas decide qué opciones de sincronización tienes. Core Data puede replicarse en CloudKit con NSPersistentCloudKitContainer. SwiftData y SQLite dejan la capa de sincronización en tus manos. Comparamos las opciones de persistencia en SwiftData vs Core Data; léelo antes de comprometerte, porque migrar más tarde un almacén sincronizado es caro.

Registra qué ha cambiado

Añade estado de sincronización a cada registro sincronizado para que la app pueda responder "¿qué falta por enviar al servidor?" sin adivinar. Un mínimo viable:

  • Un ID estable generado en el cliente (un UUID), para poder crear un registro sin conexión.
  • Una marca updatedAt y un identificador de dispositivo.
  • Un estado de sincronización: pendiente, sincronizado o en conflicto.
  • Una marca de borrado (una lápida) en lugar de un borrado definitivo, para que los borrados también se sincronicen.

Envía los cambios con una cola de salida y un URLSession en segundo plano

Una cola de salida es una tabla de mutaciones pendientes: crear este registro, cambiar este campo, borrar aquel. Cada entrada lleva un ID de petición único para que el servidor pueda ignorar un reintento que ya aplicó. El motor de sincronización vacía la cola en orden, marca las entradas como hechas al tener éxito y reintenta los fallos con una espera creciente.

Usa una sesión en segundo plano para el trabajo pesado

Para subidas y descargas grandes, usa un URLSession en segundo plano. La sesión de WWDC23 de Apple sobre transferencias reanudables afirma que una sesión en segundo plano gestiona la reanudación automáticamente tanto en descargas como en subidas, si el servidor lo admite. También espera a tener conectividad y se ejecuta fuera del proceso de tu app, así que las transferencias continúan aunque la app esté suspendida o terminada.

La misma sesión marca una división clara. Usa una sesión en segundo plano para transferencias grandes que deben persistir cuando el usuario sale de tu app y para trabajo que no es urgente, como las copias de seguridad nocturnas. Para peticiones pequeñas o urgentes, usa una sesión estándar. Activar isDiscretionary permite que el sistema elija momentos eficientes, y allowsConstrainedNetworkAccess = false te mantiene fuera del Modo de datos bajos.

Dos detalles prácticos vienen de la API y no de la charla. Las sesiones en segundo plano necesitan un delegado, y el sistema puede relanzar tu app para entregar resultados, así que debes recrear la sesión con el mismo identificador y llamar al manejador de finalización guardado desde urlSessionDidFinishEvents(forBackgroundURLSession:). Si el estado de tu cola de salida vive detrás de un actor, nuestra guía de migración a Swift 6 strict concurrency explica cómo evitar que los callbacks del delegado y el motor de sincronización entren en competencia.

Separa la sincronización inicial de la incremental

En los foros de Apple, los desarrolladores describen un patrón de sesión en segundo plano para la primera sincronización pesada y una tarea de refresco para las incrementales posteriores. Es un consejo de foro, no una guía oficial. Aun así, encaja con el comportamiento de cada pieza: la primera sincronización mueve muchos datos y las siguientes solo traen lo que cambió desde el último cursor.

Decide cómo se resuelven los conflictos

Un conflicto ocurre cuando dos dispositivos cambian el mismo registro antes de que ninguno sincronice. No puedes evitarlos, así que elige la regla a propósito.

Diagrama: mapa de nodos con tres partes: Last writer wins, Model data y Let the server arbitrate
Mapa de nodos: Decide cómo se resuelven los conflictos.

El último en escribir gana

La regla más simple conserva el cambio con la marca de tiempo más reciente. Apple la aplica en la replicación con CloudKit: la sesión de WWDC19 dice que la resolución de conflictos la implementa automáticamente NSPersistentCloudKitContainer con una política de fusión de último escritor gana. Para una persona que edita sus propios datos en un iPhone y un iPad, suele bastar.

Se queda corta con datos compartidos. Último escritor gana descarta la otra edición en silencio y asume que los relojes de los dispositivos coinciden. Si alguna de las dos suposiciones es dudosa, usa un número de versión asignado por el servidor en lugar de una marca de tiempo del cliente.

Modela los datos para evitar colisiones

La misma sesión hace una distinción útil: colaboración no es resolución de conflictos. En vez de pelear por un único campo de texto largo, divide el contenido en objetos relacionados, ordena las contribuciones de forma determinista (por fecha, por ejemplo) y registra qué dispositivo hizo cada cambio. Apple describe un árbol causal construido así como un boceto aproximado de un tipo de datos replicado libre de conflictos (CRDT). La mayoría de las apps no necesitan un CRDT completo. Fusionar por campos, de modo que sobrevivan tanto una edición del título en un dispositivo como una edición de la nota en otro, cubre los casos habituales.

Deja que el servidor arbitre

Con un backend propio, envía con cada cambio la versión que viste por última vez. Si la versión del servidor es más nueva, devuelve un HTTP 409 y el cliente fusiona y reintenta. Así la regla vive en un solo lugar y puedes cambiarla sin publicar una actualización de la app.

Una advertencia sobre CloudKit: a un desarrollador que se topó con "Unable to handle conflict reported by NSCloudKitMirroringDelegate" le respondieron que NSPersistentCloudKitContainer no admite restricciones de unicidad. La respuesta viene de un hilo de foro, así que contrástala con la documentación actual, pero planifica deduplicar en tu propio código.

La actualización en segundo plano es una pista, no un calendario

La sesión de WWDC25 sobre cómo terminar tareas en segundo plano es tajante: la ejecución en segundo plano no está garantizada, es oportunista, a menudo discrecional y está estrictamente controlada. El Modo de bajo consumo, la Actualización en segundo plano y el Modo de datos bajos afectan a la planificación.

Usa el tipo de tarea adecuado

BGAppRefreshTask sirve para obtener contenido en silencio antes de usarlo, y las apps que se usan con frecuencia reciben más oportunidades de ejecución. BGProcessingTask encaja con trabajo más pesado, como el mantenimiento de bases de datos, y puede exigir alimentación externa y conectividad de red. Para trabajo corto que debe terminar una vez iniciado, beginBackgroundTask da un poco de tiempo extra.

Para usarlas, registra el identificador de la tarea al arrancar y añádelo a la clave BGTaskSchedulerPermittedIdentifiers del Info.plist. Después envía una petición con un earliestBeginDate. Esa fecha es un límite inferior, no una cita.

Gestiona la expiración con limpieza

Cuando el sistema quiere recuperar el tiempo, llama a tu manejador de expiración. El consejo de Apple es responder con rapidez: piensa en el manejador como una oportunidad para cambiar una variable y que la tarea se detenga con elegancia. Pase lo que pase, llama a setTaskCompleted. Siempre. Como la ventana puede cerrarse en cualquier momento, guarda el progreso de forma incremental, pronto y a menudo, y mantén el trabajo atómico para que la siguiente ejecución continúe donde se quedó esta.

Añade disparadores que no dependan del planificador

Sincroniza cada vez que la app pase a primer plano y de nuevo cuando vuelva la conectividad. Una notificación push silenciosa puede avisar a la app de que el servidor tiene datos nuevos; la guía de configuración de notificaciones push cubre la parte de APNs, pero Apple señala que las notificaciones en segundo plano se consideran siempre discrecionales, así que trátalas como otra pista más.

En iOS 26, BGContinuedProcessingTask permite que el trabajo que una persona inició en primer plano, como una exportación grande, continúe cuando sale de la app. Debe empezar con una acción explícita del usuario e informar del progreso, así que encaja con tareas iniciadas por el usuario y no con la sincronización automática.

Prueba los casos sin conexión en un dispositivo

La mayoría de los errores offline solo aparecen en hardware real con fallos de red reales. Repasa esto antes de cada versión:

  1. Activa el modo avión, haz ediciones, fuerza el cierre de la app, reconecta y confirma que la cola de salida se vacía.
  2. Limita la conexión con Network Link Conditioner e interrumpe una subida grande a mitad.
  3. Edita el mismo registro en dos dispositivos sin conexión y conéctalos después en cada orden posible.
  4. Prueba una compilación de release, no solo una sesión con el depurador. Un desarrollador de los foros de Apple informó de transferencias en segundo plano que funcionaban en Xcode pero no en una compilación de release; es un testimonio anecdótico, pero es barato descartarlo.

Si estás integrando esto en un producto y quieres una segunda opinión sobre el diseño de la sincronización, nuestro equipo hace este trabajo dentro de desarrollo iOS, y puedes escribirnos desde /contact.

Conclusión

Escribe en local, envía mediante una cola de salida, decide tu regla de conflictos antes de publicar y usa la actualización en segundo plano como un extra sobre las sincronizaciones al abrir la app y al reconectar. Si la app es correcta cuando ninguna tarea en segundo plano se ejecuta, también lo será cuando una sí lo haga.

Preguntas frecuentes

¿Qué significa offline-first para una app iOS?

Significa que la app lee y escribe en un almacén local y trata la red como un asunto de segundo plano. El usuario nunca espera a una petición para ver o guardar sus propios datos, y la sincronización se ejecuta cuando hay conexión y una oportunidad en segundo plano.

¿Puede iOS garantizar que la sincronización en segundo plano se ejecute?

No. Apple describe la ejecución en segundo plano como oportunista y a menudo discrecional, y ajustes como el Modo de bajo consumo y la Actualización en segundo plano cambian lo que se ejecuta. Sincroniza también al pasar a primer plano y al reconectar.

¿Debo usar CloudKit o crear mi propia sincronización?

Usa la replicación de CloudKit con NSPersistentCloudKitContainer cuando los datos pertenezcan a la cuenta de iCloud de un solo usuario y el último escritor gana sea aceptable. Crea tu propia sincronización cuando necesites un backend compartido, fusiones por campo o tus propias reglas de conflicto.

¿Basta con que gane el último escritor?

Para los datos de una sola persona en varios dispositivos, a menudo sí. Para datos compartidos o colaborativos descarta ediciones en silencio, así que modela los datos para evitar colisiones o fusiona por campo.