Cada vez que Apple presenta un framework nuevo surge la misma duda: ¿me paso ya o espero? Con SwiftData pasa exactamente eso. En la WWDC parece magia: le pones @Model a una clase y ya tienes persistencia. Pero si tu app lleva años funcionando con SQLite, la pregunta no es si SwiftData es bonito, sino si te compensa el cambio.
En este artículo comparamos las dos opciones con código real, sin marketing, y al final tienes una guía rápida para decidir según tu caso.
¿Qué es SwiftData y por qué Apple lo diseñó?
SwiftData es el nuevo framework de persistencia de Apple que reemplaza (o más bien, envuelve de forma elegante) a Core Data. A diferencia de las herramientas anteriores, SwiftData fue diseñado desde cero para aprovechar al máximo las características modernas del lenguaje Swift, como las macros introducidas en Swift 5.9.
Al utilizar SwiftData, la definición de tu modelo de datos es tan simple como añadir la macro @Model a una clase estándar de Swift. No requiere editores gráficos de modelos (.xcdatamodeld), archivos XML complejos ni código autogenerado difícil de depurar. Curiosamente, por debajo SwiftData sigue guardando los datos en un archivo SQLite a través de Core Data, así que la comparación es en realidad entre trabajar con una capa de alto nivel o con el motor directamente.
Principales ventajas de SwiftData:
- Integración nativa con SwiftUI: Funciona de forma transparente con macros como
@Query, permitiendo que la interfaz de usuario reaccione en tiempo real a los cambios de la base de datos sin controladores intermedios. - Sintaxis declarativa y moderna: El código boilerplate se reduce drásticamente. Las consultas, relaciones y restricciones se gestionan con código Swift puro, y desde iOS 18 puedes declarar índices y unicidad con
#Indexy#Unique. - Seguridad de tipos (Type-Safety): Los predicados (
#Predicate) se comprueban en tiempo de compilación, lo que evita los errores de sintaxis SQL. Eso sí, algunas expresiones compilan pero no están soportadas por el almacén y fallan al ejecutarse, así que conviene probarlas. - Sincronización con CloudKit: Al igual que Core Data, ofrece integración lista para usar con iCloud (base de datos privada), aunque obliga a que las propiedades sean opcionales o tengan valor por defecto y no admite restricciones de unicidad.
SQLite Nativo: El veterano de la persistencia que nunca muere
SQLite es, probablemente, el motor de base de datos más desplegado en el mundo. Presente en prácticamente cualquier sistema operativo moderno, su implementación en iOS a través de la librería en C (libsqlite3) o mediante wrappers ligeros como SQLite.swift o GRDB ha sido el estándar de facto para desarrolladores que buscan control absoluto.
SQLite no abstrae la base de datos detrás de objetos opacos; te da acceso directo a un motor relacional transaccional (ACID), permitiéndote escribir SQL puro, afinar índices, gestionar transacciones atómicas y analizar planes de consulta (EXPLAIN QUERY PLAN).
Principales ventajas de SQLite:
- Rendimiento predecible y optimizable: Tienes control sobre los cursores, memoria y ejecución de consultas complejas.
- Multiplataforma: La lógica de base de datos puede compartirse entre iOS, Android, macOS, Linux o WebAssembly (mediante Kotlin Multiplatform o C++).
- Compatibilidad histórica: Funciona en cualquier versión de iOS existente, desde iOS 2.0 hasta la actualidad.
- Acceso a funcionalidades avanzadas: Soporte completo para búsqueda de texto completo (FTS5), extensiones espaciales (R*Tree) y funciones SQL personalizadas.
El mismo modelo en SwiftData y en SQLite
Para ver la diferencia en la práctica, así se guarda y se consulta una lista de tareas pendientes con cada enfoque.
Con SwiftData
import SwiftData
import SwiftUI
@Model
final class Tarea {
var titulo: String
var completada: Bool
var fecha: Date
init(titulo: String, completada: Bool = false, fecha: Date = .now) {
self.titulo = titulo
self.completada = completada
self.fecha = fecha
}
}
struct TareasView: View {
@Environment(\.modelContext) private var context
@Query(filter: #Predicate<Tarea> { !$0.completada }, sort: \Tarea.fecha)
private var pendientes: [Tarea]
var body: some View {
List(pendientes) { tarea in
Text(tarea.titulo)
}
.toolbar {
Button("Añadir") {
context.insert(Tarea(titulo: "Nueva tarea"))
}
}
}
}
Solo falta añadir .modelContainer(for: Tarea.self) a la escena principal de tu App. La lista se actualiza sola cada vez que insertas o modificas una tarea.
Con SQLite nativo
import Foundation
import SQLite3
private let SQLITE_TRANSIENT = unsafeBitCast(-1, to: sqlite3_destructor_type.self)
final class TareasDB {
private var db: OpaquePointer?
init(path: String) {
sqlite3_open(path, &db)
sqlite3_exec(db, """
CREATE TABLE IF NOT EXISTS tareas (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titulo TEXT NOT NULL,
completada INTEGER NOT NULL DEFAULT 0,
fecha REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_tareas_fecha ON tareas(fecha);
""", nil, nil, nil)
}
func insertar(titulo: String) {
var stmt: OpaquePointer?
sqlite3_prepare_v2(db, "INSERT INTO tareas (titulo, fecha) VALUES (?, ?)", -1, &stmt, nil)
sqlite3_bind_text(stmt, 1, titulo, -1, SQLITE_TRANSIENT)
sqlite3_bind_double(stmt, 2, Date().timeIntervalSince1970)
sqlite3_step(stmt)
sqlite3_finalize(stmt)
}
func pendientes() -> [String] {
var stmt: OpaquePointer?
var resultado: [String] = []
sqlite3_prepare_v2(db, "SELECT titulo FROM tareas WHERE completada = 0 ORDER BY fecha", -1, &stmt, nil)
while sqlite3_step(stmt) == SQLITE_ROW {
resultado.append(String(cString: sqlite3_column_text(stmt, 0)))
}
sqlite3_finalize(stmt)
return resultado
}
}
Por brevedad se omite la comprobación de errores: en producción revisa el código que devuelve cada llamada sqlite3_*. A cambio de escribir más código, controlas el esquema, los índices y la consulta exacta que se ejecuta, pero tienes que avisar tú a la interfaz cuando cambian los datos.
Comparativa técnica: SwiftData vs SQLite cara a cara
Para tomar una decisión fundamentada, es necesario comparar ambos enfoques en los aspectos críticos del desarrollo:
| Característica | SwiftData | SQLite Nativo / Wrappers |
|---|---|---|
| Curva de aprendizaje | Muy baja para desarrolladores Swift/SwiftUI | Media/Alta (requiere dominar SQL) |
| Requisitos de SO | iOS 17+, macOS 14+, watchOS 10+ | Sin restricciones (iOS 2+) |
| Control de bajo nivel | Limitado (abstracción sobre Core Data) | Absoluto (gestión manual de transacciones y memoria) |
| Verificación de consultas | En compilación (Predicate macros) | En tiempo de ejecución (salvo uso de generadores) |
| Sincronización CloudKit | Nativa y automática | Manual (debes programar la lógica) |
| Concurrencia | Integrada con Swift Concurrency (ModelActor) |
Manual (gestión de colas, bloqueos y WAL mode) |
¿Cuándo merece la pena migrar a SwiftData?
La migración a SwiftData es tentadora y, en muchos casos, la decisión arquitectónica correcta. Vale la pena plantear la transición en los siguientes escenarios:
1. Desarrollas proyectos nuevos (Greenfield) exclusivamente para iOS 17 o superior
Si tu aplicación no requiere dar soporte a versiones anteriores de iOS, comenzar con SwiftData acelerará la velocidad de entrega. La velocidad con la que puedes crear un prototipo funcional y llevarlo a producción es incomparable con el enfoque manual de SQLite.
2. Tu interfaz está construida al 100% en SwiftUI
SwiftData y SwiftUI están diseñados como dos mitades del mismo ecosistema. La reactividad proporcionada por @Query actualiza tus vistas de forma automática y óptima cada vez que un registro cambia, reduciendo cientos de líneas de código de observación, delegates y notificaciones.
3. Operaciones CRUD estándar y relaciones convencionales
Si tu aplicación gestiona modelos de datos tradicionales (usuarios, notas, tareas, listas de elementos) con relaciones uno-a-uno o uno-a-muchos directas, SwiftData gestiona el ciclo de vida del grafo de objetos de manera eficiente sin que tengas que diseñar esquemas SQL complejos a mano.
4. Quieres eliminar el código boilerplate y reducir deuda técnica
Mantener capas de mapeo objeto-relacional (ORM) artesanales sobre SQLite consume tiempo y es propenso a errores humanos (como tipos nulos mal gestionados o fugas de memoria en cursores abiertos). SwiftData se encarga de este trabajo invisible.
¿Cuándo es mejor mantenerte en SQLite nativo?
A pesar del empuje de Apple, SQLite nativo sigue siendo la mejor opción en situaciones concretas. No deberías migrar si cumples alguna de las siguientes condiciones:
1. Necesitas soporte para versiones anteriores a iOS 17
La adopción de nuevos sistemas operativos en el mundo corporativo suele tardar meses o años. Si tu base de usuarios activa aún cuenta con cuotas significativas en iOS 15 o iOS 16, SwiftData no es una opción viable hoy.
2. Arquitecturas multiplataforma (KMP o lógica compartida en C/Rust)
Si tu aplicación móvil forma parte de una estrategia donde la base de datos y las reglas de negocio se comparten entre Android e iOS mediante Kotlin Multiplatform (KMP), SQLite (por ejemplo con SQLDelight) es la opción natural. SwiftData es propietario y exclusivo del ecosistema Apple.
3. Consultas extremadamente complejas y procesamiento masivo de datos
Cuando necesitas realizar inserciones masivas de cientos de miles de registros en segundo plano, agregaciones analíticas complejas (JOINs múltiples, funciones ventana) o búsquedas complejas con FTS5, la capa de abstracción de SwiftData puede convertirse en un cuello de botella. SQLite permite exprimir cada ciclo del procesador y ajustar el rendimiento con micro-optimizaciones.
4. Ya cuentas con una arquitectura sólida sobre GRDB o SQLite.swift
Si tu base de código ya es estable, tiene pruebas unitarias completas y el rendimiento es óptimo, el costo de oportunidad de reescribir la capa de datos a SwiftData raramente justifica el beneficio comercial.
Estrategia de transición: Cómo abordar el salto de SQLite a SwiftData
Si tras evaluar tu proyecto decides migrar, no intentes hacer un cambio radical de una sola vez. Sigue esta estrategia para minimizar riesgos:
- Aísla la capa de datos: Asegúrate de que tu aplicación utiliza el patrón repositorio. Tu interfaz debe depender de protocolos y no de modelos directos de SQLite o SwiftData.
- Diseña el nuevo esquema en SwiftData: Define tus clases
@Modelreplicando las entidades actuales. - Crea un script de migración en el primer inicio: Al arrancar la versión actualizada, detecta si existe el archivo
.sqliteantiguo, lee los registros en lotes mediante tu adaptador anterior, insértalos en el nuevoModelContainery valida la integridad de los datos antes de borrar la base de datos obsoleta. - Implementa tests de regresión exhaustivos: Verifica que las relaciones, eliminaciones en cascada y tipos opcionales se comporten exactamente igual que en tu implementación relacional previa.
Errores típicos que conviene evitar
- Migrar porque sí. Si tu capa de SQLite funciona y está probada, reescribirla no aporta nada a tus usuarios.
- Olvidar la versión mínima. SwiftData exige iOS 17. Mira en App Store Connect qué parte de tus usuarios está por debajo antes de decidir.
- Probar solo con datos de ejemplo. Con 50 registros todo va rápido. Haz las pruebas con un volumen parecido al de producción.
- No tener plan de vuelta atrás. Conserva el archivo SQLite antiguo hasta confirmar que la migración ha ido bien en todos los dispositivos.
Conclusión: ¿cuál elegir?
Si empiezas una app nueva para iOS 17 o superior y la interfaz es SwiftUI, SwiftData te va a ahorrar mucho código y tiempo. Si tu app ya funciona con SQLite, necesitas soportar versiones antiguas o compartes la base de datos con Android, lo sensato es quedarte donde estás.
Y no tiene por qué ser todo o nada: puedes empezar usando SwiftData en una funcionalidad nueva y aislada, ver cómo se comporta en producción y decidir después con datos reales.
Artículos relacionados: El sustituto de Core Data: Guía práctica de SwiftData en proyectos reales · Adiós a las bases de datos pesadas: Cómo usar SQLite nativo en Swift para persistencia local ligera · Node.js/PHP a Swift: Cómo sincronizar bases de datos locales (SQLite) con tu backend mediante Webhooks