wandres.dev
PERSISTENCIA ALTERNATIVA · Core Data, archivos, Keychain

Archivos: FileManager, el contenedor y Codable a disco

Los directorios de una app Apple y qué significa cada uno, escritura atómica y coordinada con FileManager, serializacion de tipos con Codable, y la regla real de qué se respalda en iCloud y qué el sistema puede borrar sin avisarte.

⏱ 17 min

Debajo de cualquier almacén sofisticado hay archivos, y una app que sabe usarlos directamente gana un control que ninguna abstracción le da: el formato, el momento de escribir, la política de respaldo y el destino de cada byte. Pero el sistema de archivos de una app Apple no es un disco libre: es un contenedor con direcciones que significan cosas distintas, y el sistema actúa sobre ellas según reglas que conviene conocer antes de que te borre algo.

🎯 Al terminar esta lección sabrás
  • Situar los directorios del contenedor y elegir el correcto según la naturaleza del dato.
  • Escribir y leer tipos propios con Codable de forma atómica y segura frente a interrupciones.
  • Distinguir qué se incluye en la copia de seguridad y qué el sistema puede purgar.
  • Usar FileManager con URLs, no con rutas de texto, y entender por qué importa.

El contenedor y sus direcciones

Una app se ejecuta dentro de un contenedor aislado con una estructura fija, y cada directorio comunica una intención distinta al sistema. Documents guarda datos generados por el usuario que la app no puede regenerar: se respalda y, si lo declaras, el usuario lo ve en la app Archivos. Library/Application Support guarda datos que la app necesita pero que el usuario no manipula directamente: bases de datos, índices, estado interno. Se respalda igual, pero queda fuera de la vista.

Library/Caches es lo que su nombre promete: contenido que la app puede volver a obtener. No se respalda y el sistema lo purga cuando el almacenamiento escasea, incluso mientras la app está suspendida. Y tmp es el más volátil de todos: sirve para intermedios de una operación y el sistema lo vacía a su criterio entre ejecuciones.

📁

Documents

Datos del usuario no regenerables. Se respalda. Visible si lo declaras.

🗃️

Application Support

Estado interno y bases de datos. Se respalda. Invisible para el usuario.

♻️

Caches

Contenido regenerable. No se respalda y el sistema puede borrarlo.

tmp

Intermedios de corta vida. El sistema lo vacía cuando quiere.

Las direcciones se piden siempre al gestor de archivos, nunca se construyen concatenando texto: la ruta real cambia entre ejecuciones y entre dispositivos, y una ruta guardada ayer puede no existir hoy.

let fm = FileManager.default

let soporte = try fm.url(for: .applicationSupportDirectory,
                         in: .userDomainMask,
                         appropriateFor: nil,
                         create: true)

let destino = soporte.appending(path: "notas.json")
⚠️
Nunca persistas una URL absoluta

El contenedor de la app cambia de ubicación entre instalaciones y actualizaciones del sistema. Guardar la URL completa de un archivo y reutilizarla más tarde produce el clásico fallo que solo aparece tras actualizar. Persiste el nombre o la ruta relativa y reconstruye la URL en cada arranque; para archivos fuera del contenedor, usa un marcador de posición seguro en lugar de la ruta.

Trabajar con URLs en lugar de con cadenas de texto no es un detalle estético: una URL de archivo lleva consigo la codificación correcta, el manejo de caracteres especiales y la posibilidad de leer y escribir recursos asociados como la exclusión de copia de seguridad. Una ruta de texto pierde todo eso y reaparece en forma de fallos con nombres de archivo que contienen espacios o acentos.

Codable a disco sin perder datos

Serializar un tipo propio es directo, pero la parte interesante no es codificar: es escribir sin dejar el archivo a medias. Una escritura no atómica interrumpida por una terminación del sistema deja un archivo truncado que la próxima lectura no podrá decodificar, y el usuario pierde todo el conjunto, no la última operación.

struct Nota: Codable, Identifiable {
    let id: UUID
    var titulo: String
    var cuerpo: String
    var modificada: Date
}

func guardar(_ notas: [Nota], en url: URL) throws {
    let codificador = JSONEncoder()
    codificador.dateEncodingStrategy = .iso8601
    codificador.outputFormatting = [.sortedKeys]
    let datos = try codificador.encode(notas)
    try datos.write(to: url, options: [.atomic, .completeFileProtection])
}

func cargar(desde url: URL) throws -> [Nota] {
    guard FileManager.default.fileExists(atPath: url.path(percentEncoded: false)) else {
        return []
    }
    let decodificador = JSONDecoder()
    decodificador.dateDecodingStrategy = .iso8601
    return try decodificador.decode([Nota].self, from: Data(contentsOf: url))
}

La opción atómica hace que el sistema escriba a un archivo temporal y lo mueva sobre el destino en una operación indivisible: o está el contenido viejo o está el nuevo, nunca una mezcla. La opción de protección completa cifra el archivo con la clave del dispositivo y lo vuelve ilegible mientras el dispositivo está bloqueado, lo cual es correcto para datos sensibles y catastrófico para datos que una tarea en segundo plano necesita leer con la pantalla apagada.

flowchart LR
A[Tipos Codable en memoria] --> B[Encoder a Data]
B --> C[Escritura atomica a archivo temporal]
C --> D[Movimiento sobre el destino]
D --> E[Archivo coherente en disco]
E --> F[Decoder al arrancar]
F --> A
style E fill:#a6e3a1,color:#11111b

Cuando el archivo lo comparten varios procesos — la app, un widget, una extensión de compartir — la atomicidad deja de bastar, porque dos procesos pueden escribir a la vez y el último movimiento gana. Ahí entra el contenedor compartido de un grupo de apps y la coordinación explícita de accesos.

let compartido = FileManager.default
    .containerURL(forSecurityApplicationGroupIdentifier: "group.ejemplo.app")!
    .appending(path: "estado.json")

let coordinador = NSFileCoordinator()
var errorCoordinacion: NSError?
coordinador.coordinate(writingItemAt: compartido, options: .forReplacing, error: &errorCoordinacion) { url in
    try? datos.write(to: url, options: .atomic)
}

La regla mental es sencilla: la escritura atómica protege frente a interrupciones, la coordinación protege frente a concurrencia entre procesos, y son problemas distintos. Necesitas la primera siempre y la segunda solo cuando hay más de un proceso, pero confundirlas produce corrupciones que solo se reproducen en dispositivos reales bajo carga.

Qué se respalda y qué desaparece

La copia de seguridad de iCloud incluye el contenedor de la app salvo dos exclusiones: tmp y Library/Caches nunca viajan, y cualquier archivo marcado explícitamente como excluido tampoco. Esa marca es un recurso del propio archivo y se aplica por elemento, no por directorio.

var url = destino
var recursos = URLResourceValues()
recursos.isExcludedFromBackup = true
try url.setResourceValues(recursos)

Excluir es obligatorio, no opcional, cuando el archivo es regenerable y voluminoso: modelos descargados, imágenes cacheadas, índices reconstruibles. Subir gigabytes regenerables a la copia del usuario consume su cuota, ralentiza cada respaldo y es uno de los motivos recurrentes de rechazo en la revisión de Apple.

Hay un caso intermedio que merece nombre propio: el contenido descargable que el usuario eligió tener sin conexión. No es regenerable en silencio, porque volver a obtenerlo exige red y tiempo, pero tampoco es irremplazable. La convención madura es guardarlo en soporte de aplicación, excluirlo de la copia de seguridad y registrar en un almacén pequeño qué elementos deberían estar presentes, de modo que la app pueda detectar una ausencia y ofrecer la descarga en lugar de fingir que el dato nunca existió.

La cara opuesta del mismo criterio también importa: si guardas en Caches algo que el usuario considera suyo, un día el sistema lo purgará bajo presión de almacenamiento y para esa persona será, sencillamente, que tu app perdió sus datos. La distinción no es técnica sino semántica, y solo tú puedes hacerla.

El directorio es una declaración de intenciones ante el sistema

Elegir un directorio no es elegir dónde cae el archivo: es firmar un acuerdo con el sistema operativo sobre quién manda sobre ese byte. En Documents le dices que ese contenido es del usuario, irremplazable, y que debe sobrevivir a un cambio de teléfono; el sistema responde respaldándolo y no tocándolo jamás. En Caches le dices lo contrario — puedo regenerarlo — y el sistema toma esa afirmación al pie de la letra y borra cuando le conviene, sin avisar, sin registro y sin que tu app se entere hasta que intente leer. Esa asimetría explica dos clases enteras de fallos que se ven en producción: las apps que pierden datos del usuario porque los pusieron donde declararon que eran desechables, y las apps rechazadas o mal valoradas porque inflaron la copia de seguridad con contenido que podían volver a descargar. Ninguna de las dos es un error de programación: son errores de declaración, cometidos al escribir una línea que parecía trivial. Escribe cada ruta preguntándote qué le estás prometiendo al sistema, porque el sistema va a cumplir su parte del trato con una literalidad que no perdona.

⚔️ Ordena el contenedor de tu app
  1. Enumera los archivos que tu app escribe y anota en qué directorio cae cada uno hoy.
  2. Reclasifícalos según la pregunta clave: si desaparece, ¿puedes regenerarlo sin intervención del usuario?
  3. Convierte una escritura no atómica en atómica y comprueba el resultado matando la app durante la escritura.
  4. Marca como excluido de la copia de seguridad un archivo regenerable y verifica el recurso leyéndolo de vuelta.
  5. Borra a mano el contenido de Caches en el simulador y comprueba que tu app arranca sin errores y se recupera.