Core Data en Swift: NSManagedObject, NSFetchRequest, NSPersistentContainer y migraciones

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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP