SwiftData en 2026: persistencia de datos con el framework nativo de Apple

Core Data lleva siendo el framework de persistencia de Apple desde iOS 3. Es potente, pero su API arrastra una complejidad considerable: NSManagedObject, contextos, fetch requests con predicados de string, migraciones manuales. Con iOS 17 y macOS 14 (WWDC 2023), Apple presentó SwiftData como el sucesor moderno: mismo motor SQLite por debajo, pero una API completamente nueva basada en macros Swift, @Observable y async/await. En 2026, con iOS 18, SwiftData ha ganado estabilidad y nuevas capacidades que lo convierten en la opción por defecto para persistencia en apps Apple nuevas.

Definir modelos con @Model

La diferencia más visible con Core Data es cómo se definen los modelos. En SwiftData, un modelo es una clase Swift normal anotada con @Model:

import SwiftData

@Model
final class Libro {
    var titulo: String
    var autor: String
    var añoPublicacion: Int
    var leido: Bool = false
    var fechaAgregado: Date = Date.now

    @Relationship(deleteRule: .cascade)
    var notas: [Nota] = []

    init(titulo: String, autor: String, año: Int) {
        self.titulo = titulo
        self.autor = autor
        self.añoPublicacion = año
    }
}

@Model
final class Nota {
    var contenido: String
    var fecha: Date = Date.now

    init(contenido: String) {
        self.contenido = contenido
    }
}

La macro @Model genera automáticamente todo el código necesario para que SwiftData gestione la persistencia: propiedades observables, claves primarias, esquema de base de datos. No hay fichero .xcdatamodeld, no hay subclase de NSManagedObject.

Configurar el contenedor

El ModelContainer es el equivalente al NSPersistentContainer de Core Data. En SwiftUI se configura a nivel de la app:

@main
struct BibliotecaApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [Libro.self, Nota.self])
    }
}

Esto crea automáticamente la base de datos SQLite en el directorio de documentos de la app, con el esquema derivado de los modelos. Para configuración personalizada (por ejemplo, base de datos en memoria para tests):

let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Libro.self, configurations: config)

Insertar, borrar y modificar datos

Las operaciones se hacen a través del ModelContext, que se obtiene del entorno en SwiftUI:

struct AñadirLibroView: View {
    @Environment(.modelContext) private var contexto
    @State private var titulo = ""
    @State private var autor = ""

    var body: some View {
        Form {
            TextField("Título", text: $titulo)
            TextField("Autor", text: $autor)
            Button("Guardar") {
                let libro = Libro(titulo: titulo, autor: autor, año: 2024)
                contexto.insert(libro)
                // No hace falta llamar a save() manualmente;
                // SwiftData guarda automáticamente en momentos clave
            }
        }
    }
}

Para borrar:

contexto.delete(libro)
// O borrado en bloque:
try contexto.delete(model: Libro.self, where: #Predicate { $0.leido == true })

Consultas con @Query

@Query es la forma idiomática de recuperar datos en SwiftUI. Actualiza la vista automáticamente cuando los datos cambian:

struct ListaLibrosView: View {
    @Query(sort: Libro.titulo) private var libros: [Libro]

    // Con filtro:
    @Query(
        filter: #Predicate<Libro> { $0.leido == false },
        sort: Libro.fechaAgregado,
        order: .reverse
    )
    private var librosNoLeidos: [Libro]

    var body: some View {
        List(librosNoLeidos) { libro in
            Text(libro.titulo)
        }
    }
}

Los predicados se escriben con la macro #Predicate, que es Swift tipado en lugar de strings de NSPredicate. El compilador verifica que las propiedades existen y que los tipos son correctos.

Novedades de iOS 18: historial y sincronización

iOS 18 añade dos características importantes a SwiftData:

DataStore personalizado

En iOS 18, SwiftData permite implementar backends de almacenamiento propios a través del protocolo DataStore. Por defecto usa SQLite, pero puedes implementar un backend en memoria, JSON, o cualquier base de datos personalizada.

Unique constraints y atributos adicionales

@Model
final class Usuario {
    @Attribute(.unique)
    var email: String

    @Attribute(.externalStorage)
    var avatar: Data?  // Almacenado fuera de SQLite para datos grandes

    var nombre: String

    init(email: String, nombre: String) {
        self.email = email
        self.nombre = nombre
    }
}

SwiftData fuera de SwiftUI

SwiftData funciona perfectamente sin SwiftUI, útil para backends o tests:

let container = try ModelContainer(for: Libro.self)
let contexto = ModelContext(container)

// Insertar
let libro = Libro(titulo: "Clean Code", autor: "Robert Martin", año: 2008)
contexto.insert(libro)
try contexto.save()

// Consultar
let descriptor = FetchDescriptor<Libro>(
    predicate: #Predicate { $0.autor.contains("Martin") },
    sortBy: [SortDescriptor(.titulo)]
)
let libros = try contexto.fetch(descriptor)

Migración desde Core Data

Si tienes un proyecto existente con Core Data, SwiftData puede coexistir con él durante la migración. Apple proporciona ModelConfiguration con la opción de conectar a un stack Core Data existente. La migración completa es un proceso gradual: puedes usar SwiftData para modelos nuevos y mantener Core Data para los existentes hasta migrarlos.

SwiftData complementa muy bien el ecosistema de SwiftUI en iOS 18: las vistas con @Query se actualizan automáticamente ante cambios en la base de datos, igual que @Observable actualiza las vistas ante cambios en memoria. El modelo mental es el mismo, la escala de los datos es diferente.

Imagen: Pexels / panumas nikhomkhai

COMPARTE ESTE ARTÍCULO

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