Saltar a contenido

Serialización

Serialización/Codificación de datos

Desde la versión 4 de Swift la serialización de datos en iOS se ha uniformizado en lo que se denomina encoding/decoding. El encoding es el proceso que nos permite pasar de una estructura de datos en memoria a otro formato más adecuado para archivar o transmitir la información, por ejemplo JSON, XML,... El decoding es el proceso inverso.

El protocolo Codable

Para que un objeto sea "codificable/decodificable" debe implementar este protocolo. En la mayoría de casos no será necesario escribir nada de código, siempre que nuestra clase/struct esté compuesta por campos que sean conformes con Codable. Muchos tipos básicos de Swift lo son, como los String, Int, Float, Date, Array...

En realidad Codable no es más que una combinación de los protocolos, Encodable (para codificar) y Decodable (para decodificar):

Como ejemplo, supongamos que tenemos una estructura Alumno que representa un alumno de un determinado curso o asignatura. Su definición podría ser algo como:

struct Alumno  {
    var nombre : String
    var nota: Float
    var fechaNacimiento: Date
}

Como los campos de la clase son conformes a Codable para que la propia clase lo sea nos basta con declararla como tal:

struct Alumno : Codable {
    //..el resto es exactamente igual
}

Ahora solo necesitamos un "encoder", una clase capaz de transformar algo Codable en un formato determinado. En los APIs de iOS tenemos encoders para transformar a/desde formatos como JSON o XML. En la siguiente sección veremos un ejemplo.

Almacenar Codable en archivos

Una posibilidad es transformar nuestros datos Codable en JSON, XML o un formato para el que tengamos un encoder y luego almacenar el formato resultante en un archivo.

Veamos un ejemplo con JSON:

//Esto se usa solo para poder generar fechas a partir de cadenas,
//no está relacionado directamente con la serialización
let df = DateFormatter() 
df.dateFormat = "dd-MM-yyyy"
//Definir un alumo cuyos datos guardaremos
let alumno = Alumno(nombre: "Pepe", nota: 10.0, fechaNacimiento: df.date(from: "10/10/2000")!)

//Convertir el alumno a JSON
let encoder = JSONEncoder()
let datos = try! encoder.encode(alumno)

//Almacenarlo en un archivo llamado "result.json" en la carpeta Documents
var urlDocs = FileManager.default.urls(for:.documentDirectory,
                                       in:.userDomainMask)[0]
let urlFichero = urlDocs.appendingPathComponent("result.json")
try! datos.write(to: urlFichero)

En JSON hay algunos tipos de datos, como las fechas, que no están estandarizados. El JSONEncoder soporta varios formatos, que podemos seleccionar a través de la propiedad dateEncodingStrategy. Por ejemplo para guardar las fechas en formato ISO8601:

encoder.dateEncodingStrategy = .iso8601

Para leer los datos del archivo usaríamos un JSONDecoder

let datosLeidos = try Data(contentsOf: urlFichero)
let decoder = JSONDecoder()
let alumnoLeido = try decoder.decode(Jugador.self, from: datosLeidos)

Si al almacenar los datos hubiéramos cambiado el formato de fecha, tendríamos que cambiarlo en la propiedad dateDecodingStrategy del decoder.

Otra opción en lugar de usar JSON o XML es usar las clases NSKeyedArchiver y NSKeyedUnarchiver que sirven para "archivar" y "desarchivar" Codables, respectivamente.

Para codificar los datos usamos el método encodeEncodable de NSKeyedArchiver

//Alumno que luego guardaremos
let df = DateFormatter()
df.dateFormat = "dd-MM-yyyy"
let alumno1 = Alumno(nombre: "Pepe", nota: 10.0, fechaNacimiento: df.date(from: "10/10/2000")!)
//El archivo se llamará "datos.dat" dentro del directorio de documentos de la app
let urlDocs = FileManager.default.urls(for:.documentDirectory,  
                                       in:.userDomainMask)[0]
let urlArchivo = urlDocs.appendingPathComponent("datos.dat")
let archiver = NSKeyedArchiver()
do {
   //codificamos  
   try archiver.encodeEncodable(alumno1, forKey: NSKeyedArchiveRootObjectKey)
   //los datos codificados están en "encodedData". Los guardamos en el archivo
   try archiver.encodedData.write(to: urlArchivo)
} catch {
    print(error)
}

Para el paso contrario (decoding) usamos el método decodeTopLevelDecodable de NSKeyedUnarchiver:

//aquí "urlArchivo" tiene el mismo valor que en el ejemplo anterior
let datos = try Data(contentsOf: urlArchivo)
let unarchiver = NSKeyedUnarchiver(forReadingWith: datos)
if let alumnoLeido = try unarchiver.decodeTopLevelDecodable(Alumno.self, 
                                      forKey: NSKeyedArchiveRootObjectKey) {
   print(alumnoLeido.nombre) //Pepe
}

En realidad estas clases existen desde antes de la introducción de Codable. Se usaban para guardar datos mediante el mecanismo que existía anteriormente en iOS, denominado NSCoding, y se extendieron para poder trabajar también con Codable.

Configurar Codable

En algunas ocasiones nos puede interesar serializar/deserializar los datos usando nombres de campos distintos a los que usamos en nuestras estructuras de datos. Por ejemplo en muchas ocasiones nos tendremos que comunicar con servicios REST cuyo JSON use nombres que nos pueden resultar extraños en nuestro código, o que no se adaptan a las convenciones de Swift.

La asociación entre los nombres de los campos en el formato serializado y en nuestro código se puede definir en una enumeración de Strings llamada CodingKeys que debe ser conforme al protocolo CodingKey.

Supongamos que en el ejemplo del struct Alumno queremos que en nuestra estructura de datos la fecha se siga llamando fechaNacimiento pero en el formato serializado sea fecha_nacimiento. Lo haríamos del siguiente modo:

struct Alumno  {
    var nombre : String
    var nota: Float
    var fechaNacimiento: Date
    private enum CodingKeys : String, CodingKey {
        case nombre
        case nota
        case fechaNacimiento = "fecha_nacimiento"
    }
}