Si ya has leído el primer artículo de codables sabrás como usar los codables de forma simple. Pero este protocolo de Swift 4 tiene muchísima más potencia y complejidad que podemos aplicar para todos los casos de uso.
Para usar toda la potencia de los codables deberemos sobre escribir sus métodos e inicializadores.
Vamos a ver varios casos complejos en los que implementar este protocolo.
Codables Swift 4
Codables como tipo
Hasta ahora hemos visto que los codables se usan para mapear JSON. y que las clases o struct que implementan el protocolo se corresponden con un JSON y los atributos del objeto deben coincidir con los del JSON. En este caso vamos a asignar nuestros propios tipos como si de un Int o un String se tratara.
Esto nos puede ser muy útil por ejemplo si en los servicios nos llegan las fechas como time stamp y nosotros en la app queremos usar Date. En este caso guardaremos el valor como time stamp pero lo asignamos a un objeto del tipo DateModel.
JSON:
{
"name":"Pepe",
"date":1541423186
}
Type:
struct DateModel: Codable{
private var timeStamp:Float
var date:Date {
set {
self.timeStamp = Float(newValue.timeIntervalSince1970)
}
get {
let doubleTS = Double(self.timeStamp)
let date = Date(timeIntervalSince1970: doubleTS)
return date
}
}
public init(from decoder: Decoder) throws {
let value = try decoder.singleValueContainer()
timeStamp = try value.decode(Float.self)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(timeStamp)
}
}
Clase final:
struct Person: Codable{
var name:Stringvar date:DateModel
}
Lo primero que vemos es que estamos usando los métodos e inits de Encodable y Decodable. Una cosa curiosa del init es esta línea:
let value = try decoder.singleValueContainer()
Lo que estamos haciendo ahí es sacar un contenedor de un único valor, lo que quiere decir que este codable solo va a tener una propiedad. En este caso una propiedad privada timeStamp.
Con este tipo de codable no nos hace falta el CodingKey para asignar nombres ya que solo habrá un tipo, así que en la siguiente línea asignaremos a nuestra propiedad timeStamp el único valor que hay dentro del contenedor.
timeStamp = try value.decode(Float.self)
Con esto ya tenemos el protocolo Decodable terminado pero tenemos que terminar también Encodable para que se cumpla el protocolo Codable.
Para ello escribimos la función encode que también es muy sencilla. Solo le diremos que cree un contenedor único mediante la línea
var container = encoder.singleValueContainer()
y que le asigne al contenedor el valor de timeStamp.
try container.encode(timeStamp)
Y ya con eso hemos terminado. Como podemos ver timeStamp es una variable privada ya que solo trabajaremos con la propiedad date que no es más que una propiedad que convierte el valor de timeStamp a Date para que sea comprensible en la app y que cuando se asigna convierte el valor de un Date a timeStamp.
Custom Codable
Hay muchas veces que el desarrollador de back recibe una iluminación y decide separar los objetos con un montón de saltos en el JSON. Algo así:
{
"data":{
"name":"Pepe",
"age":34
},
"address":"Paseo de la Castellana S/N",
"Other_Data":{
"id":2
}
}
Si algún desarrollador de back manda algo así… Pero… puede pasar y nosotros no nos podemos dejar intimidar. Para eso los codables ofrecen una solución para abstraernos en el código de este tipo de personas. Lo primero es escribir la base de nuestro objeto que quedaría así:
struct Person: Codable {
var id: Int
var name: String
var age: Int
var address: String
}
Ahora tenemos que escribir varios enums con los CodingKey de cada salto. Para hacerlo de forma ordenada lo sacamos a una extensión.
extension Person{
private enum BaseContainer: String, CodingKey{
case address
case data
case otherData = "Other_Data"
}
private enum DataContainer: String, CodingKey{
case name
case age
}
private enum OtherDataContainer: String, CodingKey{
case id
}
}
Con esto lo que haremos será sacar del decoder un contenedor para cada salto. Ahora vamos a por el init. Pongo como queda y luego explico línea a línea.
extension Person{
public init(from decoder: Decoder) throws {
let baseContainer = try decoder.container(keyedBy: BaseContainer.self)
self.address = try baseContainer.decode(String.self, forKey: .address)
let dataContainer = try baseContainer.nestedContainer(keyedBy: DataContainer.self, forKey: .data)
self.name = try dataContainer.decode(String.self, forKey: .name)
self.age = try dataContainer.decode(Int.self, forKey: .age)
let otherDataContainer = try baseContainer.nestedContainer(keyedBy: OtherDataContainer.self, forKey: .otherData)
self.id = try otherDataContainer.decode(Int.self, forKey: .id)
}
}
Primero extraemos el contenedor base que tendrá como propiedades “data”,”Other_Data” y “address”. Para eso usamos la línea:
let baseContainer = try decoder.container(keyedBy: BaseContainer.self)
En la que hacemos referencia al enum BaseContainer. Luego lo primero que hacemos es extraer el valor para la propiedad “address” del contenedor base.
self.address = try baseContainer.decode(String.self, forKey: .address)
Luego le decimos al contenedor que de sus propiedades nos saque el siguiente contenedor que se corresponderá con el enum DataContainer y que se llama “data”
let dataContainer = try baseContainer.nestedContainer(keyedBy: DataContainer.self, forKey: .data)
De “dataContainer” extraeremos los valores para “name” y “age” y luego seguiremos los mismos pasos para el contenedor OtherDataContainer.
Desgraciadamente, para guardar nuestros datos debemos reformatearlos de la misma forma que vienen del servicio para que el init y el encoder se entiendan y para que el dev de back que pretende arruinarnos el día reciba correctamente su JSON infernal. Para ello volvemos a formar los contenedores y a hacer el encode.
public func encode(to encoder: Encoder) throws {
var baseContainer = encoder.container(keyedBy: BaseContainer.self)
try baseContainer.encode(self.address, forKey: .address)
var dataContainer = baseContainer.nestedContainer(keyedBy: DataContainer.self, forKey: .data)
try dataContainer.encode(self.name, forKey: .name)
try dataContainer.encode(self.age, forKey: .age)
var otherDataContainer = baseContainer.nestedContainer(keyedBy: OtherDataContainer.self, forKey: .otherData)
try otherDataContainer.encode(self.id, forKey: .id)
}
Lo que hacemos aquí es volver a encapsular toda la información dentro de la maraña de JSON de nuestro backend.
Conclusión
Los codables es una herramienta que debemos saber usar a la perfección ya que como vemos tiene mucha potencia y nos vale para casi todos los casos de uso. Para lo único que no se pueden usar codables por ejemplo es para struct o clases genéricas. Para ese tipo de parseo deberemos hacerlo manual. Pero para el resto si que podemos usar el protocolo Codable y ya dependerá del caso si lo haremos de forma sencilla o más compleja.
¡¡Aún hay más!!
Nos puede pasar que estemos intentando hacer un decode de una colección (array) de objetos y que en dichos objetos surja algún dato erróneo o inconsistente. Esto pasa sobre todo con aplicaciones que acaban de realizar una migración de datos o que tienen ya muchas versiones y los datos pueden venir de versiones muy antiguas.
El problema que tiene el protocolo codable para estos casos es que si falla el decoder de un único objeto dentro de un array falla todo el decode. Pero Apple confía en que sus desarrolladores saben usar toda la potencia de Swift y sabrán buscarle la vuelta a este inconveniente. ¡Efectivamente!
A este arreglo le e dado el nombre de Decodable+Failable.swift
Lo primero es un struct genérico que tratará los errores en el decoder y devolverá nil. Tan sencillo como esto:
fileprivate struct Failable<T: Decodable>: Decodable{
let obj:T?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.obj = try? container.decode(T.self)
}
}
Después de lo anterior ya debemos saber que significa cada línea.
Ahora haremos una extensión a Array donde el elemento cumpla el protocolo Decodable que quedaría así:
extension Array where Element: Decodable{
enum ArrayDecodeError: String, Error{
case nilData
}
static func decode(with data: Data?) throws -> [Element] {
guard let data = data else { throw ArrayDecodeError.nilData }
let arr = try JSONDecoder().decode([Failable<Element>].self, from: data)
let elements = arr.compactMap{ $0.obj }
return elements
}
}
Lo que hacemos primero es declarar un enum para nuestro error (Yo soy de objc y prácticamente no trato los errores en Swift pero si alguien los trata puede hacer el error más descriptivo).
Luego declaramos una función estática a la que le pasamos un data y devuelve un array de elementos. Lo que hacemos aquí es sencillo (Línea a línea).
- Si data es nil tiramos un error
- Hacemos un decode de un array de objetos failable con el elemento del array
- Sacamos a un nuevo array todos los elementos de failable que no sean nil. Para ello usamos compactMap
- Devolvemos un nuevo array con los elementos que se han podido decodificar correctamente.
Dejo aquí el fichero Decodable+Failable.swift
Por: Álvaro Royo
iOS Senior Developer | Instructor de “Superpoderes iOS” en el Bootcamp Desarrollo Mobile de KeepCoding.
Si tienes algo que deseas compartir o quieres formar parte de KeepCoding, escríbenos a [email protected]