Decodificando json com Swift 4 Parte 2

Decodificando json com Swift 4 Parte 2

Na primeira parte do artigo, que pode ser lido aqui, vimos como utilizar o protocolo Codable para decodificar arquivos json nativamente no Swift 4. Neste artigo criaremos um projeto utilizando a API do Unsplash.

Iniciando o Projeto

Iremos utilizar a API do Unsplash no nosso projeto e pra que o mesmo funcione é necessário uma conta no site unsplash.com/developers. Após criar a conta clique em Your applications e depois em New Application. Preencha as informações necessárias. Para o nosso exemplo é necessário apenas a opção Public Access em Permissions.
Unsplash New Application
Após a criação do projeto será gerado um Application ID e um Secret que iremos utilizar nas chamadas à API.
Swift Photo Search App
Criei um projeto no Xcode para servir de ponto inicial. Você pode baixar o projeto aqui.
Após baixar o projeto abra as propriedades do projeto e altere o nome e as opções de certificado.
Swift Photo Serach General
Informe o Application ID no arquivo Router.swift. Caso queira saber mais informações sobre este enum você pode ler o artigo Enums para o nosso bem do Daniel Bonates.

enum Router {
  ...

  struct Unsplash {
    static let ApplicationID = "YOUR_APPLICATION_ID"
    static let Secret = "YOUR_APPLICATION_SECRET"
    static let version = "v1"
  }

  ...
}

Rode o aplicativo e irá aparecer o tableview vazio.
Blank Swift Photo Search
A API do Unsplash tem uma quantidade limitada de requisições e para facilitar o desenvolvimento iremos utilizar um arquivo json local durante os testes iniciais. Para gerar a string json iremos utilizar o site codebeautify.org. clique no botão Load Url. Utilize a url https://api.unsplash.com/photos?client_id=YOUR_APPLICATION_ID substituindo YOUR_APPLICATION_ID pelo sei Application ID.
codebeautify.org
Será gerado o json de resposta da requisição.
Unsplash photos response
Clique no botão Minify, copie o conteúdo gerado e cole no arquivo File.json do projeto.
File.json
A requisição retorna um array de photos com a seguinte estrutura.

{
  "id": "hekXr0XiKH4",
  "created_at": "2018-02-28T10:42:02-05:00",
  "updated_at": "2018-03-01T11:40:46-05:00",
  "width": 1935,
  "height": 2582,
  "color": "#0C1D1F",
  "description": null,
  "categories": [],
  "urls": {
    "raw": "https://images.unsplash.com/photo-1519832444084-c1fd3b5065cf?ixlib=rb-0.3.5&ixid=eyJhcHBfaWQiOjE5NDY0fQ&s=46f040b9ddc25be44d71fd364083e024",
    "full": "https://images.unsplash.com/photo-1519832444084-c1fd3b5065cf?ixlib=rb-0.3.5&q=85&fm=jpg&crop=entropy&cs=srgb&ixid=eyJhcHBfaWQiOjE5NDY0fQ&s=8fc490eb07dc86104df82f8e89b3b10d",
    "regular": "https://images.unsplash.com/photo-1519832444084-c1fd3b5065cf?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=1080&fit=max&ixid=eyJhcHBfaWQiOjE5NDY0fQ&s=1ab00b4a60f92ea06ee8184c7195fcd1",
    "small": "https://images.unsplash.com/photo-1519832444084-c1fd3b5065cf?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=400&fit=max&ixid=eyJhcHBfaWQiOjE5NDY0fQ&s=a4630913159c491fe8be7dc89a02a415",
    "thumb": "https://images.unsplash.com/photo-1519832444084-c1fd3b5065cf?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&ixid=eyJhcHBfaWQiOjE5NDY0fQ&s=7ba86d827be0a6b873f3663dbe182a07"
  },
  "links": {
    "self": "https://api.unsplash.com/photos/hekXr0XiKH4",
    "html": "https://unsplash.com/photos/hekXr0XiKH4",
    "download": "https://unsplash.com/photos/hekXr0XiKH4/download",
    "download_location": "https://api.unsplash.com/photos/hekXr0XiKH4/download"
  },
  "liked_by_user": false,
  "sponsored": false,
  "likes": 2,
  "user": {
    "id": "pk2Y3mWcLh8",
    "updated_at": "2018-03-01T06:30:39-05:00",
    "username": "fahrulazmi",
    "name": "Fahrul Azmi",
    "first_name": "Fahrul",
    "last_name": "Azmi",
    "twitter_username": "fahrulazmi",
    "portfolio_url": null,
    "bio": "A web + iOS + Android developer. Doing photography for fun.",
    "location": "Kuala Lumpur, Malaysia",
    "links": {
      "self": "https://api.unsplash.com/users/fahrulazmi",
      "html": "https://unsplash.com/@fahrulazmi",
      "photos": "https://api.unsplash.com/users/fahrulazmi/photos",
      "likes": "https://api.unsplash.com/users/fahrulazmi/likes",
      "portfolio": "https://api.unsplash.com/users/fahrulazmi/portfolio",
      "following": "https://api.unsplash.com/users/fahrulazmi/following",
      "followers": "https://api.unsplash.com/users/fahrulazmi/followers"
    },
    "profile_image": {
      "small": "https://images.unsplash.com/profile-1510718221248-9b5d09c81e9f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=32&w=32&s=64530b1445952e6ab95e10331069fa0e",
      "medium": "https://images.unsplash.com/profile-1510718221248-9b5d09c81e9f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=64&w=64&s=89e43bbfd70da839830c5d5bc754aa14",
      "large": "https://images.unsplash.com/profile-1510718221248-9b5d09c81e9f?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&cs=tinysrgb&fit=crop&h=128&w=128&s=2ba2acfbfa66b8f3e08793ae258ac51c"
    },
    "total_collections": 3,
    "instagram_username": "fahrulazmi",
    "total_likes": 0,
    "total_photos": 18
  },
  "current_user_collections": []
}

No diretório Model do nosso projeto temos as structs Photo e User com a mesma estrutura do json. Vamos adicionar o protocolo Codable a estas structs.

Photo

struct Photo: Codable {
  ...
}

extension Photo {
  enum orderBy: String {
    ...
  }
}

extension Photo {
  struct URLs: Codable {
    ...
  }
}

User

struct User: Codable {
  ...
}

extension User {
  struct Links: Codable {
    ...
  }
}

extension User {
  struct ProfileImages: Codable {
    ...
  }
}

O protocolo Codable também funciona nativamente para o tipo URL. Não sendo necessário tratamento especial pra urls.
No arquivo MainViewController.swift vamos alterar o método LoadPhotos2() para realizar a decodificação do json que salvamos no File.json.

private func loadPhotos2() {
  loadPhotosFromFile { data in
    do {
      let decoder = JSONDecoder()
      let photos = try decoder.decode([Photo].self, from: data)

      DispatchQueue.main.async {
        self.result.update(with: photos)

        self.tableView.reloadData()
      }
    } catch DecodingError.dataCorrupted(let context) {
      print(context.debugDescription)
    } catch DecodingError.keyNotFound(let codingKey, let context) {
      print("Key: \(codingKey). \(context.debugDescription)")
    } catch DecodingError.typeMismatch(_, let context) {
      print(context.debugDescription)
    } catch {
      print(error.localizedDescription)
    }
  }
}

Após decodificar o arquivo passamos o resultado para o objeto result através do método update(with:) e chamamos o método reloadData() do tableview.
Por último iremos alterar o método viewDidLoad() substituindo o comentário // por loadPhotos2().

override func viewDidLoad() {
  super.viewDidLoad()

  ...

  loadPhotos2()
}

Se rodarmos o projeto novamente teremos o erro a seguir.

Key: createdAt. No value associated with key createdAt ("createdAt").

O erro ocorre porque o json possui o campo created_at e o nosso struct possui o campo createdAt. Para corrigir isto teremos que criar o enum CodingKeys e implementar os métodos init(from:) trhows e encode(to:) throws.

struct Photo {
  ...
}

extension Photo: Codable {
  private enum CodingKeys: String, CodingKey {
    case id
    case createdAt = "created_at"
    case updatedAt = "updated_at"
    case width
    case height
    case description
    case categories
    case urls
    case likes
    case user
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    id = try container.decode(String.self, forKey: .id)
    createdAt = try container.decode(Date.self, forKey: .createdAt)
    updatedAt = try container.decode(Date.self, forKey: .updatedAt)
    width = try container.decode(Int.self, forKey: .width)
    height = try container.decode(Int.self, forKey: .height)
    description = try container.decodeIfPresent(String.self, forKey: .description)
    categories = (try? container.decode([String].self, forKey: .categories)) ?? [String]()
    urls = try container.decode(URLs.self, forKey: .urls)
    likes = try container.decode(Int.self, forKey: .likes)
    user = try container.decode(User.self, forKey: .user)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)

    try container.encode(id, forKey: .id)
    try container.encode(createdAt, forKey: .createdAt)
    try container.encode(updatedAt, forKey: .updatedAt)
    try container.encode(width, forKey: .width)
    try container.encode(height, forKey: .height)
    try container.encode(description, forKey: .description)
    try container.encode(categories, forKey: .categories)
    try container.encode(urls, forKey: .urls)
    try container.encode(likes, forKey: .likes)
    try container.encode(user, forKey: .user)
  }
}

...

Retirei o protocolo Codable na declaração do struct e passei o mesmo para a extensão. Note que como o protocolo Codable engloba o Encodable precisamos implementar o método encode(to:) throws. Caso não queria implementá-lo altere o protocolo para Decodable.
Faremos o mesmo com a struct User.

struct User {
  ...
}

extension User: Codable {
  private enum CodingKeys: String, CodingKey {
    case id, username, name, bio, location, links
    case updatedAt = "updated_at"
    case twitterUsername = "twitter_username"
    case portfolioURL = "portfolio_url"
    case profileImages = "profile_image"
    case totalLikes = "total_likes"
    case totalPhotos = "total_photos"
  }

  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    id = try container.decode(String.self, forKey: .id)
    updatedAt = try container.decode(Date.self, forKey: .updatedAt)
    username = try container.decode(String.self, forKey: .username)
    name = try container.decode(String.self, forKey: .name)
    twitterUsername = try container.decodeIfPresent(String.self, forKey: .twitterUsername)
    portfolioURL = try container.decodeIfPresent(URL.self, forKey: .portfolioURL)
    bio = try container.decodeIfPresent(String.self, forKey: .bio)
    location = try container.decodeIfPresent(String.self, forKey: .location)
    links = try container.decode(Links.self, forKey: .links)
    profileImages = try container.decode(ProfileImages.self, forKey: .profileImages)
    totalLikes = try container.decode(Int.self, forKey: .totalLikes)
    totalPhotos = try container.decode(Int.self, forKey: .totalPhotos)
  }

  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)

    try container.encode(id, forKey: .id)
    try container.encode(updatedAt, forKey: .updatedAt)
    try container.encode(username, forKey: .username)
    try container.encode(name, forKey: .name)
    try container.encode(twitterUsername, forKey: .twitterUsername)
    try container.encode(portfolioURL, forKey: .portfolioURL)
    try container.encode(bio, forKey: .bio)
    try container.encode(location, forKey: .location)
    try container.encode(links, forKey: .links)
    try container.encode(profileImages, forKey: .profileImages)
    try container.encode(totalLikes, forKey: .totalLikes)
    try container.encode(totalPhotos, forKey: .totalPhotos)
  }
}

Rodando o projeto novamente iremos encontrar o erro:

Expected to decode Double but found a string/data instead.Double

O erro está ocorrendo devido ao tipo do campo data não estar configurado corretamente. Para corrigí-lo basta adicionar a linha abaixo no método loadPhotos2().

private func loadPhotos2() {
  loadPhotosFromFile { data in
  do {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601
    let photos = try decoder.decode([Photo].self, from: data)

    ...
  }
}

Com estas modificações o nosso aplicativo irá mostrar as photos do json.
Swift Photo Search
Vamos alterar o método loadPhotos(page:) para buscar as photos direto da API do Unsplash. Basta substituir o comentário pelo mesmo código do método loadPhotos2().

private func loadPhotos(page: Int = 1) {
  task?.cancel()

  let params: [String: Any] = ["page": page, "per_page": 20, "order_by": Photo.orderBy.latest.rawValue]

  if let request = Router.photos(params).request {
    task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
      if let error = error {
        print(error.localizedDescription)
      } else if let response = response as? HTTPURLResponse, response.statusCode == 200, let data = data {
        do {
          let decoder = JSONDecoder()
          decoder.dateDecodingStrategy = .iso8601
          let photos = try decoder.decode([Photo].self, from: data)

          DispatchQueue.main.async {
            if self.result.page == 0 {
              self.result = Result(photos)
            } else {
              self.result.update(with: photos)
            }

            self.tableView.reloadData()
          }
        } catch DecodingError.dataCorrupted(let context) {
          print(context.debugDescription)
        } catch DecodingError.keyNotFound(let codingKey, let context) {
          print("Key: \(codingKey). \(context.debugDescription)")
        } catch DecodingError.typeMismatch(let type, let context) {
          print(context.debugDescription + "\(type)")
        } catch {
          print(error.localizedDescription)
        }
      }
    })

    task?.resume()
  }
}

Altere o método viewDidLoad() para chamar o novo método.

override func viewDidLoad() {
  super.viewDidLoad()

  ...

  loadPhotos()
}

Rode o projeto novamente. O nosso applicativo estará mostrando as fotos diretamente da API. A medida que o usuário for visualizando as fotos a API é chamada novamente. Veja o código no método tableview(_:willDisplay:forRowAt:).

Swift Photo Search 2 Swift Photo Search 3 Swift Photo Search 4

Caso o usuário realize a rolagem do tableView para baixo aparecerá uma searchBox na tela permitindo que o mesmo escolha uma palavra chave para busca. Vamos implementar o código necessário para funcionar a pesquisa na API.
A função de pesquisa da API retorna uma estrutura diferente da estrutura utilizada anteriormente.

{
  "total": 906,
  "total_pages": 91,
  "results": [
    {
      ...
    }
  ]
}

Por este motivo iremos utilizar o struct PhotoSearchResult disponível na pasta Models.
Abra o fonte PhotoSearchResult.swift e adicione o protocolo Codable a struct.

struct PhotoSearchResult: Codable {
  let total: Int
  let totalPages: Int
  let results: [Photo]

  private enum CodingKeys: String, CodingKey {
    case total
    case totalPages = "total_pages"
    case results
  }
}

Voltando ao fonte MainViewController.swift iremos complementar o método searchPhotos(by:page:)

private func searchPhotos(by query: String, page: Int = 1) {
  task?.cancel()

  let params: [String: Any] = ["page": page, "per_page": 20, "query": query]

  if let request = Router.search(params).request {
    task = URLSession.shared.dataTask(with: request, completionHandler: { (data, response, error) in
      if let error = error {
        print(error.localizedDescription)
      } else if let response = response as? HTTPURLResponse, response.statusCode == 200, let data = data {
        do {
          let decoder = JSONDecoder()
          decoder.dateDecodingStrategy = .iso8601
          let searchResult = try decoder.decode(PhotoSearchResult.self, from: data)

          DispatchQueue.main.async {
            if self.result.query != query {
              self.result = Result(searchResult)
            } else {
              self.result.update(with: searchResult)
            }

            self.tableView.reloadData()
          }
        } catch DecodingError.dataCorrupted(let context) {
          print(context.debugDescription)
        } catch DecodingError.keyNotFound(let codingKey, let context) {
          print("Key: \(codingKey). \(context.debugDescription)")
        } catch DecodingError.typeMismatch(let type, let context) {
          print(context.debugDescription + "\(type)")
        } catch {
          print(error.localizedDescription)
        }
      }
    })

    task?.resume()
  }
}

O fonte é bem semelhante ao do método loadPhotos(page:) a diferença é que usamos o struct PhotoSearchResult para decodificar o resultado.

Swift photo Search Dog Swift Photo Search Cat Swift Photo Search Puppies Swift Photo Search Birds

Conclusão

Como pudemos observar neste applicativo o protocolo Codable é um grande recurso acrescentado no Swift 4 e torna a tarefa antes complicada de decodificar arquivos json com Swift em uma tarefa bem simples. Com isto podemos tirar algumas dependências de terceiro que usávamos para decodificar json do projeto diminuindo o tempo de compilação e o tamanho do aplicativo.

Você pode obter mais informações visualizando o código do protocolo Codable no GitHub.
No site da Apple tem o projeto de exemplo Using JSON with Custom Types para download.

Espero que tenham gostado do artigo. Qualquer dúvida, crítica ou sugestão de novos assuntos deixe um comentário abaixo. Caso queira entrar em contato pode me chamar no twitter @mateusfsilva. Se gostou compartilhe.

Um comentário em “Decodificando json com Swift 4 Parte 2

Os comentários estão encerrados.