Serialización de clases arbitrarias
Lo que Apple denomina archiving o coding es lo que en otros lenguajes normalmente se conoce como serialización o marshalling, es decir, transformar un objeto de cualquier clase o un grafo de objetos en una secuencia de bytes que se pueda almacenar en un archivo.
El protocolo NSCoding
Para que un objeto sea serializable (o “archivable”) debe implementar el protocolo NSCoding. Dicho protocolo exige que se implemente un método encode, que se debe encargar de serializar el objeto, y un inicializador especial que debe realizar la operación contraria, la deserialización.
Como ejemplo, supongamos que tenemos una clase Alumno que representa un alumno de un determinado curso o asignatura. Su definición podría ser algo como:
class Alumno {
var nombre : String?
var varon : Bool = false
var fechaNacimiento: Date?
init(nombre elNombre: String, esVaron varon : Bool,
nacido fechaNac: Date) {
self.nombre = elNombre
self.varon = varon
self.fechaNacimiento = fechaNac
}
}
Si queremos hacer que esta clase sea serializable debemos hacer que implemente el protocolo NSCoding y además que herede de NSObject. Lo primero sería declararlo en la cabecera de la clase:
class Alumno : NSObject, NSCoding {
Ahora tendremos que implementar los métodos de serialización/deserialización. Vamos con el primero, cuyo prototipo debe ser algo como:
func encode(with aCoder: NSCoder) {
El NSCoder es el objeto que nos permite serializar. Con su ayuda tenemos que ir uno por uno serializando los campos del objeto actual. Contamos con una familia de métodos encode(valor, forKey:clave), que nos permiten trabajar con Int, Double, Bool, etc, o objetos en general. No todos los objetos son automáticamente serializables de este modo. Por ejemplo sí lo son String, Date (puedes consultar la documentación estándar y ver que ambas clases son conformes a NSCoding). También son "automáticamente serializables" las colecciones (Array, Set y Dictionary) de esos tipos.
Nótese el forKey del método de serialización. A cada campo que queremos serializar debemos asociarle un nombre simbólico (una clave). Esto nos permite deserializar luego los campos en el orden que queramos, simplemente especificando en cada caso la clave adecuada.
Teniendo en cuenta todo lo comentado, el código del método de serialización podría ser algo del estilo:
func encode(with aCoder: NSCoder) {
aCoder.encode(nombre, forKey:"nombre")
aCoder.encode(varon, forKey:"varon")
aCoder.encode(fechaNacimiento, forKey:"fechaNacimiento")
}
No siempre es necesario serializar todos los campos. Si alguno de ellos se pudiera reconstruir a partir de los otros o de otra información no sería necesario serializarlo para poder “reconstituir” luego el objeto.
Para deserializar hacemos uso de la familia de métodos decodeXXX(forKey:) de la clase NSCoder, donde XXX es uno de los tipos Bool, Int, Object, ... El método de deserialización es un inicializador que recibe como parámetro un NSCoder:
required init?(coder aCoder : NSCoder) {
self.nombre = aCoder.decodeObject(forKey: "nombre") as? String
self.fechaNacimiento = aCoder.decodeObject(forKey: "fechaNacimiento") as? Date
self.varon = aCoder.decodeBool(forKey: "varon")
}
Como vemos nos limitamos a deserializar todos los campos. Si usamos el decodeObject tenemos que hacer la conversión al tipo final con as. Nótese que el orden de deserialización es indiferente y no tiene por qué ser igual que el de la serialización ya que cada campo tiene su clave.
Una cuestión interesante es que el proceso de archivado o serialización es recursivo. Es decir, archivar cada campo de un objeto puede no ser una operación atómicas si el campo es un tipo propio. Por ejemplo supongamos que tenemos una clase Grupo que representa un grupo de objetos Alumno que forman parte de un curso:
class Grupo {
var nombre : String?
var miembros : [Alumno] = []
init(nombre elNombre: String, miembros losMiembros: [Alumno]) {
self.nombre = elNombre
self.miembros = losMiembros
}
Archivar un grupo es tan directo en términos de código como archivar un único alumno (en la clase grupo faltaría además el : NSObject, NSCoding)
func encode(with aCoder: NSCoder) {
aCoder.encode(nombre, forKey: "nombre")
aCoder.encode(miembros, forKey: "miembros")
}
Pero nótese que implica recursivamente archivar cada uno de los alumnos que lo componen. Al ejecutar el encode sobre el array de miembros, NSCoder va a llamar automáticamente al proceso de archivado para cada componente del array, proceso que ya habíamos implementado al ser Alumno conforme a NSCoding.
Archivar y “desarchivar” objetos
Hasta ahora hemos visto los hooks a definir en nuestro código cuando "alguien" ejecute el proceso de archivado/desarchivado. Para ejecutar el archivado tenemos el método estático archiveRootObject de la clase NSKeyedArchiver.
//Creamos las estructuras de datos en memoria
let a = Alumno(nombre: "Eva", esVaron: false, nacido:Date())
let a2 = Alumno(nombre: "Pepe", esVaron: true, nacido:Date())
let grupo = Grupo(nombre: "primero A", miembros: [a,a2])
//Como es un ejemplo simple, obtenemos el dir. temporal y allí creamos un archivo
let tmpDir = FileManager.default.temporaryDirectory
let archivoGrupo = tmpDir.appendingPathComponent("grupo.dat")
//Archivamos el objeto. Como segundo parámetro necesitamos un path, no una URL
NSKeyedArchiver.archiveRootObject(grupo, toFile: archivoGrupo.path)
Para el paso contrario, de "reconstituir" un grafo de objetos a partir de un archivo, usamos el método unarchiveObject de NSKeyedUnarchiver:
if let grupoLeido = NSKeyedUnarchiver.unarchiveObject(withFile: archivoGrupo.path) as? Grupo {
for alumno in grupoLeido.miembros {
print(alumno.nombre!)
}
}