Core Data es el framework de persistencia de Apple y, a pesar de su curva de aprendizaje, sigue siendo la opción más potente para almacenar datos estructurados en iOS y macOS. Con NSPersistentContainer, NSManagedObject y NSFetchRequest puedes tener un stack de persistencia completo funcionando en menos de veinte líneas de código.
Configurar el stack: NSPersistentContainer
NSPersistentContainer encapsula toda la infraestructura de Core Data: el modelo de datos, el coordinador persistente y el contexto principal. La inicialización mínima es:
import CoreData
class PersistenceController {
static let shared = PersistenceController()
let container: NSPersistentContainer
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "MiModelo") // nombre del .xcdatamodeld
if inMemory {
container.persistentStoreDescriptions.first?.url =
URL(fileURLWithPath: "/dev/null")
}
container.loadPersistentStores { _, error in
if let error {
fatalError("Core Data no pudo cargar: (error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
}
}
El flag inMemory resulta muy útil en tests y en previews de SwiftUI: los datos nunca se escriben a disco y desaparecen al terminar el proceso.
NSManagedObject: trabajar con entidades
Cada entidad del modelo genera una subclase de NSManagedObject. Con la generación automática de Xcode, cada atributo se convierte en una propiedad tipada. Para crear, leer, actualizar y eliminar objetos:
// Crear
let contexto = PersistenceController.shared.container.viewContext
let tarea = Tarea(context: contexto)
tarea.titulo = "Aprender Core Data"
tarea.completada = false
tarea.fecha = Date()
try? contexto.save()
// Leer (ver NSFetchRequest más abajo)
// Actualizar
tarea.completada = true
try? contexto.save()
// Eliminar
contexto.delete(tarea)
try? contexto.save()
Siempre hay que llamar a save() para que los cambios se persistan. El contexto almacena los cambios en memoria hasta que se guarda.
NSFetchRequest: consultas con predicados y orden
NSFetchRequest es el equivalente de una query SQL en Core Data. Permite filtrar con NSPredicate y ordenar con NSSortDescriptor:
// Todas las tareas pendientes, ordenadas por fecha
let request: NSFetchRequest = Tarea.fetchRequest()
request.predicate = NSPredicate(format: "completada == %@", NSNumber(value: false))
request.sortDescriptors = [NSSortDescriptor(keyPath: Tarea.fecha, ascending: true)]
request.fetchLimit = 50
do {
let tareasPendientes = try contexto.fetch(request)
for tarea in tareasPendientes {
print(tarea.titulo ?? "Sin título")
}
} catch {
print("Error en fetch: (error)")
}
Los predicados de Core Data usan una sintaxis de cadena con format specifiers: %@ para objetos, %d para enteros y %K para nombres de clave:
// Buscar por texto parcial (case-insensitive)
request.predicate = NSPredicate(format: "titulo CONTAINS[cd] %@", textoBusqueda)
// Rango de fechas
let inicio = Calendar.current.startOfDay(for: Date())
let fin = inicio.addingTimeInterval(86400)
request.predicate = NSPredicate(format: "fecha >= %@ AND fecha < %@",
inicio as NSDate, fin as NSDate)
Trabajo en segundo plano: performBackgroundTask
El contexto principal (viewContext) está vinculado al hilo principal. Para operaciones pesadas importar miles de registros, hacer batch updates usa un contexto en background:
PersistenceController.shared.container.performBackgroundTask { bgContext in
// Este closure se ejecuta en un hilo background
bgContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
for datos in datosImportados {
let entidad = Producto(context: bgContext)
entidad.nombre = datos.nombre
entidad.precio = datos.precio
}
do {
try bgContext.save()
// automaticallyMergesChangesFromParent propaga los cambios al viewContext
} catch {
print("Error guardando en background: (error)")
}
}
Nunca pases objetos NSManagedObject entre contextos directamente. Usa el objectID para transferir referencias:
let objectID = tarea.objectID // seguro entre hilos
PersistenceController.shared.container.performBackgroundTask { bgContext in
let tareaEnBg = bgContext.object(with: objectID) as! Tarea
tareaEnBg.completada = true
try? bgContext.save()
}
Migraciones ligeras
Cuando el modelo de datos cambia entre versiones de la app, Core Data necesita migrar los datos existentes. Las migraciones ligeras son automáticas para cambios sencillos (añadir un atributo, cambiar su nombre o valor por defecto):
// En el descriptor del store, activar migración automática
container.persistentStoreDescriptions.first?.setOption(
true as NSNumber,
forKey: NSMigratePersistentStoresAutomaticallyOption
)
container.persistentStoreDescriptions.first?.setOption(
true as NSNumber,
forKey: NSInferMappingModelAutomaticallyOption
)
Para cambios que Core Data no puede inferir automáticamente (cambios de tipo de dato, transformaciones complejas) se necesitan migraciones pesadas con un NSMappingModel explícito, que quedan fuera del alcance de este artículo.
@FetchRequest en SwiftUI
SwiftUI tiene soporte nativo de Core Data mediante @FetchRequest, que observa los cambios en el contexto y actualiza la vista automáticamente:
struct ListaTareas: View {
@FetchRequest(
sortDescriptors: [SortDescriptor(Tarea.fecha, order: .forward)],
predicate: NSPredicate(format: "completada == false"),
animation: .default
)
private var tareas: FetchedResults
@Environment(.managedObjectContext) private var contexto
var body: some View {
List(tareas) { tarea in
Text(tarea.titulo ?? "")
}
.toolbar {
Button("Añadir") {
let nueva = Tarea(context: contexto)
nueva.titulo = "Nueva tarea"
nueva.fecha = Date()
try? contexto.save()
}
}
}
}
El contexto se inyecta en el entorno desde el punto de entrada de la app:
@main
struct MiApp: App {
let persistence = PersistenceController.shared
var body: some Scene {
WindowGroup {
ListaTareas()
.environment(.managedObjectContext, persistence.container.viewContext)
}
}
}
Resumen
Core Data sigue siendo la solución de persistencia más completa para apps Apple cuando los datos tienen estructura compleja, relaciones entre entidades o necesitan soporte de migraciones. El stack moderno con NSPersistentContainer es más simple que el antiguo stack manual; NSFetchRequest con predicados ofrece la misma potencia que SQL pero tipada; y la integración con SwiftUI a través de @FetchRequest hace que la sincronización entre datos y vista sea automática. Para proyectos nuevos, SwiftData (disponible desde iOS 17) ofrece una API más moderna sobre el mismo motor subyacente.
