wandres.dev
PERSISTENCIA ALTERNATIVA · Core Data, archivos, Keychain

Core Data en 2026: cuándo sigue mandando

Que capacidades conserva Core Data frente a SwiftData en 2026, como comparten el mismo almacen SQLite y conviven en la misma app, y como se planifica una migracion entre ambos sin detener el desarrollo ni perder datos de los usuarios.

⏱ 18 min

SwiftData no sustituyó a Core Data: lo envolvió. Debajo del modelo declarativo con macros sigue latiendo el mismo motor, el mismo almacén y el mismo formato en disco. Esa continuidad es la mejor noticia de la persistencia en el ecosistema Apple, porque convierte una decisión que parecía excluyente en una cuestión de nivel de acceso, y abre la puerta a que las dos capas coexistan sobre los mismos datos.

🎯 Al terminar esta lección sabrás
  • Identificar las capacidades que Core Data conserva y SwiftData todavía no expone.
  • Explicar por qué ambos comparten almacén y qué implica eso para la compatibilidad.
  • Configurar la convivencia de las dos capas sobre un mismo fichero de datos.
  • Planificar una migración incremental entre ambos con criterios de reversibilidad.

Lo que Core Data todavía hace mejor

La primera capacidad que marca la diferencia es la migración pesada. Cuando un cambio de esquema no se puede expresar como una correspondencia mecánica entre versiones — dividir una entidad en dos, fusionar dos en una, recalcular relaciones a partir de datos existentes — Core Data ofrece modelos de correspondencia y políticas de migración personalizadas que ejecutan código arbitrario por cada objeto migrado. SwiftData cubre bien lo ligero y buena parte de lo personalizado, pero cuando la transformación es estructural el camino sigue pasando por la capa de abajo.

La segunda es el control del almacén. Core Data permite varios almacenes coordinados en el mismo contexto, configuraciones que reparten entidades entre ficheros distintos, almacenes de solo lectura empaquetados con la app y opciones finas del motor subyacente. Es la diferencia entre poder decir dónde va cada entidad y aceptar la decisión por defecto.

La tercera es la observación del historial persistente y las operaciones por lotes. Actualizar o borrar millones de filas sin materializar cada objeto en memoria, y reaccionar a los cambios que otro proceso — una extensión, un widget, una tarea en segundo plano — escribió en el mismo almacén, sigue siendo territorio donde el API de abajo manda.

🏗️

Migración pesada

Modelos de correspondencia y políticas con código por objeto para cambios estructurales.

🧩

Configuraciones

Varios almacenes coordinados y reparto explícito de entidades entre ficheros.

Lotes e historial

Actualización y borrado masivos sin materializar, y seguimiento entre procesos.

🔭

Diagnóstico

Instrumentación madura, trazas de SQL y herramientas de perfilado con años de rodaje.

Las dos últimas se aprecian mejor con código a la vista. Un borrado por lotes no instancia un solo objeto: traduce a una sentencia que el motor ejecuta directamente sobre el almacén, y el precio de esa eficiencia es que los contextos en memoria quedan desactualizados y hay que fusionarles los cambios a mano.

let peticion = NSBatchDeleteRequest(
    fetchRequest: NSFetchRequest<NSFetchRequestResult>(entityName: "Nota")
)
peticion.resultType = .resultTypeObjectIDs

let resultado = try contexto.execute(peticion) as? NSBatchDeleteResult
if let ids = resultado?.result as? [NSManagedObjectID] {
    NSManagedObjectContext.mergeChanges(
        fromRemoteContextSave: [NSDeletedObjectsKey: ids],
        into: [contexto]
    )
}

Ese detalle resume bien la relación entre las dos capas: lo que SwiftData te ahorra es exactamente la contabilidad manual del estado en memoria, y lo que Core Data te da a cambio de esa contabilidad es acceso a operaciones que no pasan por ella.

Un almacén, dos capas

La clave técnica de la convivencia es que SwiftData genera, a partir de tus tipos anotados, exactamente la misma estructura de modelo que Core Data describe en su editor gráfico. El fichero en disco es el mismo SQLite con el mismo esquema de metadatos, así que un almacén escrito por una capa es legible por la otra siempre que ambas describan las mismas entidades con los mismos nombres y tipos.

import SwiftData
import CoreData

// Lado SwiftData
@Model final class Nota {
    var titulo: String = ""
    var cuerpo: String = ""
    var creada: Date = Date.now
}

// Lado Core Data sobre el mismo fichero
let url = URL.applicationSupportDirectory.appending(path: "datos.store")

let contenedor = NSPersistentContainer(name: "Modelo")
contenedor.persistentStoreDescriptions = [NSPersistentStoreDescription(url: url)]
contenedor.loadPersistentStores { _, error in
    if let error { fatalError("almacen no cargado: \(error)") }
}

let configuracion = ModelConfiguration(url: url)
let contenedorSwiftData = try ModelContainer(for: Nota.self, configurations: configuracion)

La condición no negociable es que el modelo descrito en el editor gráfico y el modelo derivado de las macros coincidan campo a campo. Cualquier divergencia — un atributo obligatorio en un lado y opcional en el otro, un nombre de entidad distinto, una relación inversa ausente — se manifiesta como un fallo al abrir el almacén, no como una advertencia.

⚠️
La coincidencia se comprueba en ejecución

Nada en el compilador garantiza que las dos descripciones del modelo sean iguales: el editor gráfico y las macros son fuentes independientes. La única defensa razonable es una prueba automatizada que abra el mismo almacén con las dos capas en cada integración continua. Sin ella, la divergencia aparece en el dispositivo de un usuario.

flowchart TD
A[Tipos con macro Model] --> B[Descripcion de modelo derivada]
C[Editor grafico de entidades] --> D[Descripcion de modelo compilada]
B --> E[Mismo esquema logico]
D --> E
E --> F[Un unico fichero SQLite]
F --> G[Contexto de SwiftData]
F --> H[Contexto de Core Data]
style E fill:#a6e3a1,color:#11111b

Compartir almacén no significa compartir contexto. Cada capa mantiene su propio grafo de objetos en memoria, y un cambio guardado desde una no aparece en la otra hasta que esta vuelve a consultar o recibe la notificación correspondiente. Diseñar la convivencia asumiendo coherencia inmediata entre ambos contextos es la fuente número uno de datos fantasma durante la transición.

Migrar entre capas sin apagar la app

La pregunta que casi nadie hace antes de empezar es cuál es el criterio de vuelta atrás. Una migración de capa no es un experimento si no puedes revertirla, y la única forma de poder revertirla es no transformar el almacén: mientras el esquema no cambie, cambiar de capa es cambiar de código, y el código se revierte publicando la versión anterior. En cuanto la capa nueva introduce un cambio de esquema, la puerta se cierra y pasas a jugar con las reglas de la próxima lección.

La estrategia que funciona es incremental y por lectura primero. Se empieza abriendo el almacén existente con la capa nueva en modo de solo lectura, verificando que todas las entidades se materializan y que los conteos coinciden con los del lado antiguo. Solo cuando esa comprobación pasa en un corpus real de almacenes se habilita la escritura desde la capa nueva, y aún entonces conviene mantener el lado antiguo operativo para las operaciones que dependan de sus capacidades exclusivas.

// Verificacion de paridad antes de habilitar la escritura
func verificarParidad(contexto: NSManagedObjectContext,
                      modelo: ModelContext) throws -> Bool {
    let peticion = NSFetchRequest<NSNumber>(entityName: "Nota")
    peticion.resultType = .countResultType
    let viejo = try contexto.count(for: NSFetchRequest<NSFetchRequestResult>(entityName: "Nota"))
    let nuevo = try modelo.fetchCount(FetchDescriptor<Nota>())
    return viejo == nuevo
}

Conviene además decidir de antemano quién es la fuente de la verdad del esquema durante la convivencia. Mantener dos descripciones editadas a mano es sostenible unas semanas, no un año: pasado ese punto, la práctica sana es generar el modelo gráfico a partir de los tipos anotados o congelarlo y prohibir su edición, dejando las macros como única fuente. La convivencia sin una regla explícita de propiedad del esquema degenera en divergencia.

La dirección contraria también es legítima y a veces necesaria: una app nacida en SwiftData que descubre que necesita historial persistente o migración pesada puede bajar a la capa de abajo para esa operación concreta sin renunciar al modelo declarativo en el resto. La convivencia no es un estado transitorio hacia una pureza futura; es una arquitectura válida en sí misma.

El criterio de decisión para 2026, entonces, no es cuál de los dos es mejor sino dónde poner la frontera. La vista y la lógica de producto viven mejor sobre el modelo declarativo, porque ahí la ergonomía se traduce en menos código y menos errores de sincronización con la interfaz. Las operaciones de infraestructura — importaciones masivas, mantenimiento, migraciones estructurales, coordinación entre procesos — viven mejor una capa por debajo, porque ahí lo que necesitas es control, no comodidad. Una app madura tiene esa frontera dibujada de forma explícita y documentada, no descubierta por accidente cuando algo falla.

La compatibilidad de formato es lo que hace posible la evolución

Lo verdaderamente notable de este diseño no es la comodidad de las macros, sino la decisión de construir el API nuevo sobre el formato de almacenamiento del viejo. Piensa en lo que habría significado lo contrario: un formato nuevo habría obligado a cada app existente a ejecutar una migración total, unidireccional y sin retorno, sobre los dispositivos de sus usuarios, antes de poder usar una sola línea del API moderno; y la comunidad se habría partido en dos ecosistemas incompatibles durante años. Al conservar el formato, la plataforma convirtió una migración forzosa en una adopción gradual y reversible, donde el coste de probar el camino nuevo es casi cero y el de retroceder también. Esa es la propiedad que deberías perseguir en tus propias capas: cuando diseñes una abstracción sobre datos que ya existen, el valor no está en la elegancia de la interfaz nueva, sino en si mantiene intacta la representación de abajo. Una interfaz elegante que rompe el formato obliga a todos a elegir; una interfaz elegante que lo respeta deja que cada uno decida cuándo, y esa diferencia decide si tu abstracción se adopta o se ignora. En persistencia, la compatibilidad hacia atrás no es una concesión al pasado: es el mecanismo por el que un ecosistema puede seguir cambiando.

⚔️ Abre el mismo almacén desde las dos capas
  1. Crea un proyecto con un modelo de dos entidades definido en el editor gráfico y replícalo con macros.
  2. Configura ambas capas apuntando al mismo fichero y comprueba que las dos abren sin error.
  3. Escribe un registro desde una capa y léelo desde la otra en la misma ejecución.
  4. Provoca una divergencia deliberada — cambia un atributo a opcional en un solo lado — y lee el mensaje de error completo.
  5. Escribe la prueba automatizada de paridad de conteos y añádela a tu integración continua.