Core Data siempre fue el framework de persistencia de Apple para iOS, pero su API basada en Objective-C, los NSManagedObject y la verbosidad de las migraciones de esquema lo hacían complicado. SwiftData, presentado en WWDC23, es la respuesta nativa en Swift: usa macros, integra directamente con SwiftUI, y reemplaza casi todo el boilerplate de Core Data con unas pocas líneas.
@Model: definir el esquema con Swift
La macro @Model convierte una clase Swift normal en un modelo persistente. No necesitas heredar de nada ni implementar protocolos especiales:
import SwiftData
@Model
final class Tarea {
var titulo: String
var completada: Bool
var fechaCreacion: Date
var prioridad: Int
init(titulo: String, prioridad: Int = 0) {
self.titulo = titulo
self.completada = false
self.fechaCreacion = .now
self.prioridad = prioridad
}
}
La macro genera automáticamente el esquema de persistencia, los identificadores únicos y el seguimiento de cambios. Todas las propiedades almacenadas son persistidas por defecto.
Propiedades excluidas y únicas
@Model
final class Usuario {
@Attribute(.unique) var email: String // Restricción de unicidad
var nombre: String
@Attribute(.externalStorage)
var fotoPerfil: Data? // Almacenado fuera de la BD (para datos grandes)
@Transient // No se persiste
var sesionActiva: Bool = false
init(email: String, nombre: String) {
self.email = email
self.nombre = nombre
}
}
@Relationship: relaciones entre modelos
Las relaciones se definen con la macro @Relationship:
@Model
final class Proyecto {
var nombre: String
var descripcion: String
@Relationship(deleteRule: .cascade, inverse: Tarea.proyecto)
var tareas: [Tarea] = []
init(nombre: String, descripcion: String = "") {
self.nombre = nombre
self.descripcion = descripcion
}
}
@Model
final class Tarea {
var titulo: String
var completada: Bool
var proyecto: Proyecto? // Relación inversa
init(titulo: String) {
self.titulo = titulo
self.completada = false
}
}
Las reglas de borrado disponibles son: .nullify (pone la relación a nil), .cascade (borra los objetos relacionados), .deny (impide el borrado si hay objetos relacionados) y .noAction.
ModelContainer: configurar la pila de persistencia
ModelContainer es el equivalente al NSPersistentContainer de Core Data. Se configura una vez y se inyecta en el entorno de SwiftUI:
@main
struct MiApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Tarea.self, Proyecto.self])
}
}
// O con configuración personalizada:
@main
struct MiApp: App {
let contenedor: ModelContainer
init() {
let esquema = Schema([Tarea.self, Proyecto.self, Usuario.self])
let configuracion = ModelConfiguration(
schema: esquema,
isStoredInMemoryOnly: false, // true para previews y tests
allowsSave: true
)
contenedor = try! ModelContainer(for: esquema, configurations: [configuracion])
}
var body: some Scene {
WindowGroup { ContentView() }
.modelContainer(contenedor)
}
}
@Query: leer datos en SwiftUI
@Query es la forma declarativa de leer y observar datos persistidos. La vista se actualiza automáticamente cuando los datos cambian:
struct ListaTareas: View {
// Todas las tareas, ordenadas por fecha
@Query(sort: Tarea.fechaCreacion, order: .reverse)
private var tareas: [Tarea]
// Solo las tareas completadas
@Query(filter: #Predicate { $0.completada })
private var tareasCompletadas: [Tarea]
// Con predicado dinámico se reconfigura con .query()
@Query private var tareasProyecto: [Tarea]
var body: some View {
List(tareas) { tarea in
TareaRow(tarea: tarea)
}
}
}
Para predicados dinámicos basados en parámetros externos, pasa la configuración de @Query como parámetro del inicializador:
struct TareasProyecto: View {
let proyecto: Proyecto
@Query private var tareas: [Tarea]
init(proyecto: Proyecto) {
self.proyecto = proyecto
_tareas = Query(
filter: #Predicate { tarea in
tarea.proyecto?.nombre == proyecto.nombre
},
sort: .fechaCreacion
)
}
var body: some View {
List(tareas) { TareaRow(tarea: $0) }
}
}
ModelContext: operaciones CRUD
ModelContext es el equivalente al NSManagedObjectContext. Lo obtienes del entorno para insertar, borrar y guardar:
struct FormularioTarea: View {
@Environment(.modelContext) private var contexto
@State private var titulo = ""
var body: some View {
Form {
TextField("Título", text: $titulo)
Button("Guardar") { guardar() }
}
}
func guardar() {
let tarea = Tarea(titulo: titulo)
contexto.insert(tarea) // Insertar
try? contexto.save() // Persistir (opcional se autosalva)
}
}
// Borrar
func borrar(_ tarea: Tarea, context: ModelContext) {
context.delete(tarea)
}
// Borrar múltiples con predicado
func borrarCompletadas(context: ModelContext) throws {
try context.delete(model: Tarea.self,
where: #Predicate { $0.completada })
}
Migraciones de esquema
SwiftData gestiona las migraciones con VersionedSchema y SchemaMigrationPlan:
enum TareasSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] { [Tarea.self] }
@Model final class Tarea {
var titulo: String
init(titulo: String) { self.titulo = titulo }
}
}
enum TareasSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] { [Tarea.self] }
@Model final class Tarea {
var titulo: String
var prioridad: Int // Nueva propiedad
init(titulo: String, prioridad: Int = 0) {
self.titulo = titulo
self.prioridad = prioridad
}
}
}
enum TareasMigracion: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[TareasSchemaV1.self, TareasSchemaV2.self]
}
static var stages: [MigrationStage] {
[.lightweight(fromVersion: TareasSchemaV1.self,
toVersion: TareasSchemaV2.self)]
}
}
SwiftData en previews y tests
extension ModelContainer {
static var preview: ModelContainer = {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(
for: Tarea.self,
configurations: config
)
// Poblar con datos de prueba
let contexto = container.mainContext
contexto.insert(Tarea(titulo: "Tarea de ejemplo"))
contexto.insert(Tarea(titulo: "Otra tarea"))
return container
}()
}
#Preview {
ListaTareas()
.modelContainer(.preview)
}
Resumen
SwiftData elimina la brecha entre el modelo de datos y el código Swift: los modelos son clases Swift normales, las consultas son propiedades SwiftUI, y las relaciones se definen con macros en lugar de editores de esquema visuales. Para apps nuevas que soporten iOS 17+, es la elección natural. Para proyectos con Core Data existente, puedes usar ambos frameworks en paralelo durante la transición.
