El mapa de opciones: qué guarda cada almacén
Las seis vías de persistencia de una app Apple comparadas por contrato: preferencias, archivos, llavero, SQLite, Core Data y SwiftData. Qué garantiza cada una, qué volumen aguanta, qué consultas permite y dónde te deja tirado.
Toda app guarda algo. La pregunta nunca es si persistir, sino dónde, y esa decisión se toma una vez y se paga durante años. El ecosistema Apple ofrece seis vías, y no forman una escala de menor a mayor sofisticación: forman un espacio con ejes independientes — volumen, consultabilidad, seguridad, sincronización y coste de cambio. Esta lección dibuja ese espacio para que la elección deje de ser un reflejo y pase a ser un argumento defendible.
- Distinguir las seis vías de persistencia por el contrato que ofrecen, no por su popularidad.
- Situar cada una en los ejes de volumen, consulta, seguridad y sincronización.
- Reconocer los tres errores de asignación más caros y su síntoma en producción.
- Justificar una elección de almacén con criterios verificables antes de escribir la primera línea.
Seis contratos, no seis niveles
UserDefaults es un diccionario de preferencias que el sistema serializa a una lista de propiedades dentro del contenedor de la app. Se carga entera en memoria al primer acceso y se escribe de forma diferida, así que su latencia de lectura es despreciable y su latencia de escritura es invisible pero no instantánea. Su contrato es estrecho a propósito: claves pequeñas, tipos de lista de propiedades, sin transacciones, sin consultas, sin cifrado.
Los archivos son el sustrato de todo lo demás. Con FileManager decides la ruta, el formato y el momento de escribir; con Codable conviertes tus tipos a datos y de vuelta. El contrato aquí es máxima libertad y mínima ayuda: cualquier tamaño, cualquier formato, escritura atómica si la pides, y cero capacidad de consulta. Si quieres buscar un elemento, cargas el conjunto entero y filtras en memoria.
El Keychain no vive en el contenedor de la app: es una base de datos del sistema, cifrada con apoyo del hardware y gobernada por clases de accesibilidad que definen cuándo el sistema está dispuesto a descifrar cada elemento. Su contrato es seguridad real a cambio de un API incómodo y un coste por operación alto comparado con leer una preferencia.
SQLite viene incluido en el sistema y ofrece lo que ninguna de las anteriores da: transacciones con garantías, consultas declarativas, índices y millones de filas sin cargar nada superfluo en memoria. El precio es que tú escribes cada línea de mapeo entre filas y tipos de Swift, y cada error de ese mapeo es un error en tiempo de ejecución.
Core Data construye sobre SQLite un grafo de objetos gestionado: seguimiento de cambios, materialización perezosa, deshacer, contextos concurrentes y un motor de migraciones que aguanta transformaciones arbitrarias. SwiftData reexpresa ese mismo motor con macros y tipos de Swift, integrado con la vista y con menos ceremonia, a cambio de un techo de control más bajo.
UserDefaults
Preferencias y estado de interfaz. Kilobytes, no megabytes. Sin consultas ni cifrado.
Archivos
Documentos, exportaciones, cachés grandes. Control total del formato y del momento de escribir.
Keychain
Tokens, contraseñas y claves. Cifrado del sistema y política de accesibilidad explícita.
SQLite
Consultas y volumen sin intermediarios. Máximo rendimiento, máximo trabajo manual.
Core Data
Grafo de objetos con migraciones pesadas y control fino del almacén.
SwiftData
El mismo motor en Swift moderno, declarativo e integrado con la vista.
Puesto en una frase: UserDefaults recuerda preferencias, los archivos guardan documentos, el llavero custodia secretos, y los tres motores de base de datos responden preguntas. Cuando una app emplea uno de ellos para el trabajo de otro, el sistema no protesta: el fallo aparece meses después, ya con datos de usuarios dentro.
El eje que decide: consulta frente a volumen
Dos preguntas ordenan casi todo el espacio. La primera: ¿necesitas recuperar un subconjunto sin cargar el resto? Si la respuesta es sí, quedan fuera UserDefaults y los archivos, porque ambos obligan a materializar el conjunto completo antes de filtrar. La segunda: ¿el volumen crece con el uso o está acotado por diseño? Un conjunto acotado por diseño — ajustes, el último filtro elegido, una lista de favoritos con tope — tolera un almacén sin índices; un conjunto que crece con el tiempo no.
// Acotado por diseno: preferencias del usuario
UserDefaults.standard.set(true, forKey: "mostrarCompletadas")
// Crece con el uso: pertenece a un almacen consultable
@Model final class Nota {
var titulo: String
var creada: Date
init(titulo: String, creada: Date = .now) {
self.titulo = titulo
self.creada = creada
}
}
Conviene notar que estas dos preguntas se responden sobre el comportamiento esperado dentro de dos años, no sobre el conjunto de datos de la primera versión. Casi todos los almacenes mal elegidos lo fueron porque la respuesta era correcta el día que se escribió la línea. Una lista de favoritos parece acotada hasta que el producto añade sincronización, historial y búsqueda, y entonces el archivo único que funcionaba se convierte en el cuello de botella que nadie quiere tocar.
El tercer eje, la seguridad, no admite negociación por comodidad: si el dato identifica al usuario ante un servidor o descifra otros datos, va al llavero aunque quepa en una preferencia. Y el cuarto, la sincronización, condiciona el modelo desde el primer día, porque el motor que sincroniza impone que cada propiedad tenga valor por defecto o sea opcional.
flowchart TD A[Que dato quiero guardar] --> B[Es un secreto o credencial] B -->|si| K[Keychain] B -->|no| C[Necesito consultar subconjuntos] C -->|no| D[El volumen esta acotado] D -->|si| U[UserDefaults] D -->|no| F[Archivos con Codable] C -->|si| E[Necesito control fino del almacen] E -->|no| S[SwiftData] E -->|si| G[Core Data o SQLite directo] style K fill:#f9e2af,color:#11111b style S fill:#a6e3a1,color:#11111b
Una app real combina varios
Ningún proyecto serio elige un almacén y se acaba la conversación. Lo normal es una asignación por naturaleza del dato, y la señal de que el diseño es correcto es que puedes justificar cada asignación en una frase sin decir la palabra costumbre.
struct Persistencia {
// Preferencia: acotada, no secreta, sin consultas
var ordenPreferido: OrdenLista {
get { OrdenLista(rawValue: defaults.string(forKey: "orden") ?? "") ?? .fecha }
set { defaults.set(newValue.rawValue, forKey: "orden") }
}
// Secreto: al llavero, siempre
func tokenDeSesion() throws -> Data? {
try llavero.leer(servicio: "api", cuenta: "sesion")
}
// Conjunto creciente y consultable: almacen con indices
let contenedor: ModelContainer
// Regenerable y voluminoso: archivo excluido de la copia
let cacheDeImagenes: URL
}
Esa separación tiene un efecto secundario valioso: cada almacén se puede sustituir de forma independiente. El día que el conjunto de notas necesite consultas por texto completo, cambias una pieza sin tocar las preferencias ni el llavero. Si en cambio todo vive en el mismo sitio porque era lo cómodo, cualquier cambio arrastra a todo lo demás.
Los tres errores caros
El primero es guardar credenciales en UserDefaults. El archivo de preferencias es texto estructurado sin cifrar dentro del contenedor: cualquier copia de seguridad sin cifrar, cualquier extracción del dispositivo y cualquier herramienta de inspección lo leen tal cual. El síntoma llega tarde y en forma de incidente, no de fallo.
El segundo es guardar en un único archivo un conjunto que crece. Funciona con cien elementos y se degrada de forma silenciosa: cada escritura reescribe el fichero entero, cada lectura lo materializa entero, y el coste crece de forma lineal con datos que el usuario acumula durante meses. El síntoma es un arranque cada vez más lento que nadie sabe explicar.
El tercero es elegir un grafo de objetos por miedo a quedarse corto cuando el dato es un puñado de ajustes. Se paga en tiempo de compilación, en complejidad de concurrencia y en una migración obligatoria cada vez que cambia un campo trivial.
Los tres comparten una raíz: se decidieron por analogía con lo que el equipo ya usaba, no por el contrato que el dato exigía. El antídoto cabe en una pregunta que puedes hacerte en treinta segundos antes de escribir la primera línea de persistencia — qué garantiza este almacén que yo necesito, y qué necesito que no garantiza — y que ahorra semanas de trabajo dos años después.
Además de no estar cifrado, UserDefaults no ofrece atomicidad entre claves ni orden garantizado de escritura frente a una terminación abrupta. Escribir dos claves que deben ser coherentes entre sí es una condición de carrera esperando a ocurrir. Si dos datos deben cambiar juntos o no cambiar, necesitas una transacción, y eso significa otro almacén.
Cambiar de almacén no es refactorizar: es migrar. El código lo reescribes en una tarde, pero los datos que ya están en los dispositivos de tus usuarios no se reescriben solos, y no puedes pedirles que empiecen de cero. Por eso la elección de persistencia se comporta distinto a casi cualquier otra decisión técnica de una app: no es reversible por voluntad propia, solo por un procedimiento que hay que diseñar, ejecutar en cada dispositivo, y que debe funcionar a la primera porque no hay segunda oportunidad sobre unos datos ya transformados. La consecuencia práctica es que el criterio correcto no es qué almacén resuelve mejor el requisito de hoy, sino cuál sigue siendo defendible cuando el conjunto crezca dos órdenes de magnitud, cuando aparezca la necesidad de buscar, cuando el usuario espere que sus datos estén también en el iPad y cuando alguien pregunte dónde vive exactamente el token de sesión. Un ingeniero con experiencia no elige el almacén más simple ni el más potente: elige el que tiene el menor coste esperado de migración dado lo que razonablemente va a pasar. Y documenta esa apuesta, porque en dos años nadie recordará por qué.
- Lista todos los datos que tu app persiste y anota, para cada uno, en qué almacén está hoy.
- Clasifica cada dato en los cuatro ejes: volumen acotado o creciente, consultable o no, secreto o no, sincronizable o no.
- Marca las discrepancias entre la clasificación y el almacén actual, y ordénalas por coste de corregirlas más tarde.
- Localiza el archivo de preferencias de tu app en el simulador y ábrelo: comprueba con tus ojos qué hay ahí en claro.
- Elige la discrepancia más grave y escribe en tres líneas el procedimiento de migración que necesitarías. Si no sabes escribirlo, ese es tu riesgo real.