Durante o desenvolvimento de aplicativos é muito comum realizar uma comunicação com informações externas via rede. Para estabelecer esta comunicação é necessário formatar os dados tanto para enviá-los quanto para recebê-los. Em Swift esse processo é conhecido como Encodable e Decodable.
Segundo a documentação:
- Encodable: Um tipo que pode se codificar para uma representação externa.
- Decodable: Um tipo que pode decodificar a partir de uma representação externa.
Uma maneira de utilizar ambos protocolos em uma mesma entidade é conformar com o alias Codable. O Codable foi introduzido no Swift 4 como substituto ao NSCoding. Portanto, esse tipo permite codificar e decodificar para formatos diferentes.
Ao informar que uma entidade conforma com estes protocolos é necessário que todas as suas propriedades também conformem com o protocolo indicado. Muitos tipos comuns do Swift Standard Library e Foundation são codable por padrão. Quando houver um tipo customizado, é necessário que este também se conforme com o protocolo.
No exemplo a seguir podemos verificar o erro emitido pelo Xcode quando tentamos conformar o tipo Student com Codable. Como o tipo School não tem essa especificação, Student não está em total conformidade. Os demais tipo, Int e String, não geram erros pois são importados do framework Foundation:
Encoding
Como vimos, o protocolo Encodable permite o tipo para ser enviado à uma camada externa do aplicativo. É possível codificar nossos tipos para diversos formatos, como JSON, XML e Plist.
Para preparar nosso tipo com o formato JSON podemos utilizar JSONEncoder da seguinte maneira:
struct Student: Codable {
    var id: Int
    var name: String
    var school: School
}
struct School: Codable {
    var name: String
}
let school = School(name: "Swift School")
let student = Student(id: 1, name: "Steve Jobs", school: school)
let encoder = JSONEncoder()
let data = try encoder.encode(student)É possível visualizar como está a representação JSON ao converter para String:
print(String(data: data, encoding: .utf8)!){
	"id": 1,
	"name": "Steve Jobs",
	"school": {
		"name": "Swift School"
	}
}Como podemos ver, os dados são aninhados conforme estrutura declarada nas structs.
Decoding
Para realizar o processo inverso, ou seja, para decodificar instâncias de um tipo de dados a partir de objeto JSON, podemos utilizar o JSONDecoder.
Nesse processo precisamos especificar o tipo para a conversão e a origem dos dados. No exemplo abaixo foi utilizado um JSON hardcoded mas a origem pode ser de qualquer lugar.
let json = """
{
    "id": 2,
    "name": "Tim Cook",
    "school": {
        "name": "Swift School"
    }
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let studentFromJson = try decoder.decode(Student.self, from: json)Ao manipularmos o objeto que recebeu a decodificação podemos perceber que este possui todas as propriedades das structs.
print("Objeto estudante: \(studentFromJson)")
print("Nome estudante: \(studentFromJson.name)")
print("Escola do estudante: \(studentFromJson.school.name)")Chaves JSON personalizadas
Para realizar os processos de envio e recebimento das informações é necessário que as chaves dos campos estejam corretas, caso contrário, ou a API ou o nosso aplicativo não reconhecerá o dado trafegado.
Afim de evitar refatoração futura de campos, podemos definir chaves de codificação personalizadas para as propriedades. Esse processo é feito ao definir o enum que conforme com o protocolo CodingKeys:
extension Student {
    enum CodingKeys: String, CodingKey {
        case id
        case name = "nome"
        case school = "escola"
    }
}
extension School {
    enum CodingKeys: String, CodingKey {
        case name = "nome"
    }
}Todos os campos da struct devem estar listados como case. Quando esse enum existir apenas esses cases que serão utilizados na codificação e decodificação, portanto, mesmo que alguma propriedade não exija mapeamento, ela deverá ser incluída.
Após estas modificações a visualização do JSON será assim:
{
	"id": 1,
	"nome": "Steve Jobs",
	"escola": {
		"nome": "Swift School"
	}
}Modificando hierarquia JSON
Também é possível alterar a hierarquia do JSON sem modificar nosso modelo de estrutura. Essa modificação possibilita criar hierarquias simples às mais complexas com aninhamentos.
Encodable
Em codificação, é necessário implementar o método encode(to:) para especificar a estrutura. Esse processo é baseado na criação de um dicionário do tipo KeyedEncodingContainer para armazenar as propriedades.
O exemplo abaixo mostra a alteração para uma estrutura direta sem o aninhamento anterior:
extension Student {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(id, forKey: .id)
        try container.encode(school.name, forKey: .school)
    }
}A exibição do JSON fica dessa forma:
{
	"id": 1,
	"nome": "Steve Jobs",
	"escola": "Swift School"
}Quando necessário aumentar o nível do aninhamento podemos utilizar o método nestedContainer(keyedBy:forKey:). Para instanciar esse método é obrigatório informar um enum que também conforme com o protocolo CodingKey e também informar a chave de posicionamento do novo aninhamento.
No exemplo abaixo, alteramos o método encode(to:) para exibir um novo grupo de escolas:
extension Student {
    enum SchoolKeys: String, CodingKey {
        case best = "melhores_escolas"
    }
    func encode(to encoder: Encoder) throws {
        ...
        var schoolContainer = container.nestedContainer(keyedBy: SchoolKeys.self, forKey: .school)
        try schoolContainer.encode(school, forKey: .best)
    }
}O resultado do JSON será assim:
{
	"id": 1,
	"nome": "Steve Jobs",
	"escola": {
		"melhores_escolas": {
			"nome": "Swift School"
		}
	}
}Decodable
No processo de decodificação devemos implementar as modificações no construtor init(from:). Ou seja, como este é o ponto de entrada dos dados é onde devemos modificar a estrutura.
Para recebermos a seguinte estrutura:
{
    "id": 1,
    "nome": "Steve Jobs",
    "escola": "Swift School"
}Devemos modificar a entrada dessa forma:
extension Student {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        id = try container.decode(Int.self, forKey: .id)
        let schoolName = try container.decode(String.self, forKey: .school)
        school = School(name: schoolName)
    }
}Para aumentar o nível do aninhamento na entrada dos dados pode utilizar o método nestedContainer(keyedBy:forKey:):
Para adaptar à entrada do JSON abaixo:
{
	"id": 1,
	"nome": "Steve Jobs",
	"escola": {
		"melhores_escolas": {
			"nome": "Swift School"
		}
	}
}O init deve ficar dessa forma:
extension Student {
    init(from decoder: Decoder) throws {
        ...
        let schoolContainer = try container.nestedContainer(keyedBy: SchoolKeys.self, forKey: .school)
        school = try schoolContainer.decode(School.self, forKey: .best)
    }
}Datas com Codable
Pode-se dizer que, lidar com datas em programação é um sofrimento para a maiorida dos desenvolvedores. Principalmente quando é necessário trafegar datas entre cliente e servidor. O importante nesse case é atender ao fato que o envio e recebimento de datas é feito como String, mas para haver a comunicação, deve ser estabelecido um formatado para esta informação.
Portanto, vamos adicionar um novo campo com data ao JSON:
{
	"id": 1,
    "nome": "Steve Jobs",
    "data_nascimento" : "24-02-1955",
	"escola": {
		"nome": "Swift School"
	}
}Para facilitar a manipulação, podemos extender a classe DateFormatter com o formato de data do JSON. No exemplo abaixo é utilizado uma propriedade para facilitar seu reuso:
extension DateFormatter {
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "dd-MM-yyyy"
        return formatter
    }()
}Essa propriedade deve ser vinculada ao JSONEncoder e JSONDecoder nas propriedades .dateEncodingStrategy e .dateDecodingStrategy, respectivamente:
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(.dateFormatter)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(.dateFormatter)