Criando uma Pokedex com Swift Part: 2/3

Consumindo um JSON

Fabio Miciano
14 min readJan 17, 2017

Recapitulando a parte anterior.

Nós instalamos o CocoaPods em nossa máquina e a API Alamofire em nosso projeto no Xcode.

Hoje vamos começar uma nova etapa na história dessa Pokedex maravilhosa que estamos criando.

Vamos colocar as mãos no código, usar o Alamofire (finalmente), consumir um JSON, criar o parse e também um model, que vamos utilizar na terceira e última parte do tutorial.

Para os que quiserem, vou colocar o link do projeto completo abaixo.

De onde vamos buscar as informações?

Vamos usar a versão 2 de uma RESTful API, que tem suas informações atualizadas pela Bulbapedia.

A RESTful API é a pokeapi.

A Estrutura

Vamos fazer toda a implementação de request seguindo uma estrutura.

Nossa ViewController vai pedir para uma classe de Request que faça a requisição para buscar as informações dos pokemons.

A classe de Request vai implementar no handler de retorno um Response que será útil para evitar erros, até por parte do programador, por exemplo esquecer de tratar um erro 400.

Ela também vai enviar o retorno da requisição para uma outra classe que irá fazer o parse das informações, separando apenas o que queremos.

O Parse irá criar um model (Struct) que será inserido em um retorno do handler do Response.

O Response irá ser retornado para ViewController que irá consumir todo conteúdo em formato de Struct.

A criação da estrutura vai de cada programador. Eu gosto de usar esse método pois dá uma segurança no código e diminui as chances de erro por falta de atenção ou por falta de boas práticas por parte dos programadores que podem mexer no seu código futuramente.

Essa estrutura foi pensada em cima do MVVM (Model — View — ViewModel).

Modificando o Info.plist

A nossa RestFull Api não é HTTPS, por conta disso precisamos adicionar uma chave no nosso Info.plist, para que nosso app saiba que mesmo sendo um HTTP é seguro fazer essa requisição.

Abra o Info.plist do seu projeto.

Adicione as chaves com os valores iguais ao da imagem abaixo.

Configuração do Info.plist

Agora nosso projeto está configurado, vamos para próxima etapa.

Criando os Models

Os models serão structs que vão armazenar informações que serão enviadas pelo parse.

Vamos criar três models, um para a pokedex, um para o pokemon e um para erros do servidor.

O primeiro vai ser o model da Pokedex.

No Project Navigator do lado esquerdo do Xcode, clique com o botão direito em cima do projeto e depois em New Group de o nome de Models.

Na pasta Models clique com o botão direito em seguida em New File.

Escolha o tipo do arquivo como Swift File. De o nome de PokedexModel.

Seguindo o JSON que retorna da url http://pokeapi.co/api/v2/pokemon/, o nosso model precisa de campos para armazenar o count que é um inteiro, o next que é uma string com a próxima pagina, o previous que é uma string com a página anterior e o results que é um array de dicionário, com informações básicas dos pokemons.

O código ficará parecido com o abaixo.

struct PokedexModel
{
let count: Int
let next: String
let previous: String
let results: Int
}

Ainda dá pra melhorar essa Struct. Se der um erro no parse, ele precisar retornar um Model vazio, como vamos fazer?

Para resolver isso vamos criar um extension e implementar uma função de init dessa Struct, facilitando o uso em outras partes do código, caso tenha que retornar algo vazio.

O Código completo da nossa Struct ficará parecido com esse.

struct PokedexModel
{
let count: Int
let next: String
let previous: String
let results: Int
}
extension PokedexModel
{
init()
{
self.count = 0
self.next = ""
self.previous = ""
self.results = 0
}
}

Agora vamos criar nosso model que vai guardar as informações dos pokemons, seguindo o JSON que retorna da url http://pokeapi.co/api/v2/pokemon/2/ por exemplo.

Lógico que não vamos dar parse de todos os campos desse JSON. Para esse post vamos utilizar apenas três campos. O id do pokemon, o nome e uma url da imagem.

Na pasta de models crie uma nova classe Swift, igual fizemos antes, com o nome PokemonModel.

Nessa struct também irei criar o método init em um extension igual na classe acima. O código vai ficar parecido com o abaixo.

struct PokemonModel
{
var id: Int
var name: String
var urlImage: String
}
extension PokemonModel
{
init()
{
self.id = 0
self.name = ""
self.urlImage = ""
}
}

Falta o último model, que será uma struct para armazenar o retorno de erro que o request pode devolver. Eu dei o nome de ServerError.

Essa struct não terá um init em uma extension porque não queremos um model de erro vazio, se der erro ele terá que ser preenchido.

Esse model só terá uma variável para guardar a mensagem de erro e uma variável para o código do erro. O model ficará parecido com esse.

struct ServerError
{
let msgError:String
let statusCode:Int
}

E assim finalizamos a parte de criação de models, a próxima etapa é criar os parses do JSON.

Criando o Parse

Teremos que criar parse de duas requisições: a Pokedex e os Pokemons, seguindo os models que já criamos.

Recomendo pegar a url que devolve o JSON da pokedex e uma que devolve a do pokemon, para saber os nomes dos campos que vamos ter que usar.

Essas urls podem ser pegas no site da API que passei acima nesse post.

Vamos lá, clique com o botão direito em cima da pasta do projeto e crie um novo grupo chamado View Model.

Clique com o botão direito em cima da pasta View Model e crie um arquivo Swift com o nome de ParsePokedex.

Vamos criar uma função que vai dar parse da pokedex, o código ficará mais ou menos assim.

class ParsePokedex
{
func parseAllPokedex(response: [String: Any]?) -> PokedexModel
{
//Faço o unwrap do dicionario enviado pelo request, caso for nulo aciono a função de init para criar model vazio
guard let response = response else { return PokedexModel() }
//Coloco os valores do dicionario em variaveis
//Caso alguma dessas chaves estiverem vazias o valor atribuido na variavel será oque está depois do ??
let count = response["count"] as? Int ?? 0
let next = response["next"] as? String ?? ""
let previus = response["previous"] as? String ?? ""
//Preciso apenas da quantidade de filhos do array
let resultList = response["results"] as? [[String: Any]] ?? []
let results = resultList.count
return PokedexModel(count: count, next: next, previus: previus, results: results) }
}

As chaves que estão dentro do response[""], foram pegas vendo o JSON que a url retorna, não foi nada inventado.

Nesse parse nos pegamos os valores que vem do request, colocamos um tipo e atribuímos no model da pokedex.

O próximo parse é o dos pokemons. Caso você olhe o JSON dos pokemons, você verá um monte de informações, para se tornar viável vamos usar só três campos: nome, id, imagem de frente. O código desse parse ficará mais ou menos assim.

func parsePokemon(response: [String: Any]?) -> PokemonModel
{
guard let response = response else { return PokemonModel() }
let name = response["name"] as? String ?? ""
let id = response["id"] as? Int ?? 0
let sprites = response["sprites"] as? [String: Any]
let urlImage = sprites?["front_default"] as? String ?? ""
return PokemonModel(id: id, name: name, urlImage: urlImage)
}

Agora vamos colocar frescuras… apenas por questão de TOC mesmo. Se olharmos bem, temos alguns [String: Any] pelos parses. Vamos criar typealias para esses campos, para que a leitura fique fácil quando outra pessoa olhar nosso código.

Vamos criar dois typealias.

typealias ParseReponseDict = [String: Any]?
typealias PokemonSpriteDict = [String: Any]

E agora vamos aplicar em nosso código, fazendo isso a classe vai ficar assim.

typealias ParseReponseDict = [String: Any]?
typealias PokemonSpriteDict = [String: Any]
class ParsePokedex
{
func parseAllPokedex(response: ParseReponseDict) -> PokedexModel
{
guard let response = response else { return PokedexModel() }
let count = response["count"] as? Int ?? 0
let next = response["next"] as? String ?? ""
let previus = response["previous"] as? String ?? ""
let resultList = response["results"] as? [[String: Any]] ?? []
let results = resultList.count
return PokedexModel(count: count, next: next, previus: previus, results: results)
}
func parsePokemon(response: ParseReponseDict) -> PokemonModel
{
guard let response = response else { return PokemonModel() }
let name = response["name"] as? String ?? ""
let id = response["id"] as? Int ?? 0
let sprites = response["sprites"] as? PokemonSpriteDict
let urlImage = sprites?["front_default"] as? String ?? ""
return PokemonModel(id: id, name: name, urlImage: urlImage)
}
}

Agora temos nossos parses completos, faltam dois passos para completar nosso o fluxo de uso do Alamofire.

A próximo coisa que vamos criar são os responses.

Os responses são enums, que nossa classe de request vai usar e nosso View Controller também, quando for implementar o request.

Isso dá mais segurança ao código, para que não esqueçamos nenhum tratamento de erro ou sucesso

Criando os Responses

Clique com o botão direito em cima do grupo ViewModel, e crie um novo arquivo Swift, coloque o nome dele de PokedexResponses.

Vamos criar 3 responses no mesmo arquivo.

Responses nada mais são que enums com os possíveis retornos que devemos tratar na ação na qual estamos usando.

Primeiro response que vamos criar é o da pokedex, Os retornos dele são sucesso, server error, time out, e sem conexão.

Nosso primeiro response vai ficar assim.

enum PokedexResponse
{
case success(model: PokedexModel)
case serverError(description: ServerError)
case timeOut(description: ServerError)
case noConnection(description: ServerError)
}

O retorno de sucesso irá retornar nosso model da pokedex, os demais vão retornar o model de erro do servidor.

O próximo response é o do pokemon, sua estrutura é igual a da pokedex, porem o sucesso irá retornar o model de pokemons, ficando mais ou menos assim.

enum PokemonResponse
{
case success(model: PokemonModel)
case serverError(description: ServerError)
case timeOut(description: ServerError)
case noConnection(description: ServerError)
}

O JSON só nos devolve a url da imagem do pokemon, ou seja, vamos precisar de um request a mais para baixar a imagem que vamos exibir. Por conta disso, teremos mais um response relacionado à isso.

O ImageResponse tem a mesma estrutura dos anteriores porem o sucesso retorna um Data.

O melhor seria usar o AlamofireImage para fazer download da imagem, entretanto esse post não vai abordar isso. Quero mostrar apenas o uso do Alamofire e como usar uma RESTful API.

Qualquer duvida só olhar o link abaixo.

O ImageResponse vai ficar igual o código abaixo.

enum ImageResponse
{
case success(model: Data)
case serverError(description: ServerError)
case timeOut(description: ServerError)
case noConnection(description: ServerError)
}

Com isso nós finalizamos todos nossos responses e podemos pular para ultima etapa do Alamofire, os tão esperados requests.

Criando os Requests

Crie um novo arquivo dentro do grupo View Model com o nome RequestPokedex.

Nessa classe vamos ter três funções públicas, uma para o request da pokedex, uma para os pokemons e outra para as imagens.

Lembra que criamos o parse? Pois bem, vamos criar uma variável que inicia a classe.

class ResquestPokedex
{
let parse: ParsePokedex = ParsePokedex()
}

Importamos o Alamofire em nossa classe e iniciamos um objeto chamado SessionManager.

O SessionManager vai guardar as configurações de request, como tempo de timeOut por exemplo.

O código da criação do SessionManager com a importação do Alamofire vai ficar assim.

//Importação do Alamofire
import Alamofire
class ResquestPokedex
{
let alamofireManager: SessionManager = {
//Criação das configurações
let configuration = URLSessionConfiguration.default
//Tempo de timout em milisegundos
configuration.timeoutIntervalForRequest = 10000
configuration.timeoutIntervalForResource = 10000
return SessionManager(configuration: configuration)
}()
let parse: ParsePokedex = ParsePokedex()
}

Todos os nossos request vão usar uma URL base, então em cima da classe vamos criar uma struct que vai armazenar a URL Main dos request.

struct PokemonAPIURL {
static let Main: String = "http://pokeapi.co/api/v2/pokemon/"
}

Caso você precise adicionar mais URLs, basta colocá-las dentro da Struct.

Vamos escrever agora nosso primeiro request, finalmente.

O primeiro request será o da Pokedex. Ele vai nos trazer todos os pokemons com uma paginação de 20 pokemons. Por isso o nosso request vai se chamar getAllPokemons.

Ele vai ter como parâmetro uma URL e um completion para ser implementado no ViewController.

func getAllPokemons(url:String?, completion:@escaping (_ response: PokedexResponse) -> Void)
{
}

A primeira coisa que faremos na função é verificar se o parâmetro da URL veio preenchido. Caso ele venha vazio usamos a URL base da struct.

//Verifico se a URL é vazia, se for coloco a URL Main
let page = url == "" || url == nil ? PokemonAPIURL.Main : url ?? ""

Vamos chamar o nosso SessionManager e fazer um request de GET através dele, passando a URL preenchida como parâmetro.

alamofireManager.request(page, method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON
{ (response) in
}

Quando o request retornar precisamos tratar o sucesso ou o erro. Então, dentro do completion, vamos pegar o statusCode e tratar o retorno com um swicth case.

//Pega o status
let statusCode = response.response?.statusCode
switch response.result
{
//Status de erro ou sucesso
case .success(let value):
//JSON com retorno
let resultValue = value as? [String: Any]
if statusCode == 404
{
if let description = resultValue?["detail"] as? String
{
let error = ServerError(description: description, errorCode: statusCode!)
completion(.serverError(description: error))
}
}
else if statusCode == 200
{
let model = self.parse.parseAllPokedex(response: resultValue)
completion(.success(model: model))
}
case .failure(let error):
//Status de erro
let errorCode = error._code
if errorCode == -1009
{
let erro = ServerError(description: error.localizedDescription, errorCode: errorCode)
completion(.noConnection(description: erro))
}
else if errorCode == -1001
{
let erro = ServerError(description: error.localizedDescription, errorCode: errorCode)
completion(.timeOut(description: erro))
}
}

As estruturas de tratamento do sucesso e do erro nos request são muito semelhantes, mudando basicamente dentro dos ifs.

E nosso request para pegar todos os pokemons fica assim.

func getAllPokemons(url:String?, completion:@escaping PokedexCompletion)
{
let page = url == "" || url == nil ? PokemonAPIURL.Main : url!
alamofireManager.request(page, method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON
{ (response) in
//Pega o status
let statusCode = response.response?.statusCode
switch response.result
{
//Status de erro ou sucesso
case .success(let value):
//JSON com retorno
let resultValue = value as? [String: Any]
if statusCode == 404
{
if let description = resultValue?["detail"] as? String
{
let error = ServerError(description: description, errorCode: statusCode!)
completion(.serverError(description: error))
}
}
else if statusCode == 200
{
let model = self.parse.parseAllPokedex(response: resultValue)
completion(.success(model: model))
}
case .failure(let error):
//Status de erro
let errorCode = error._code
if errorCode == -1009
{
let erro = ServerError(description: error.localizedDescription, errorCode: errorCode)
completion(.noConnection(description: erro))
}
else if errorCode == -1001
{
let erro = ServerError(description: error.localizedDescription, errorCode: errorCode)
completion(.timeOut(description: erro))
}
}
}
}

Com esse request completo teremos informações básicas dos pokemons. Mas ainda precisamos da imagem dele. Para isso vamos usar um outro request que retorna as informações mais detalhadas.

Como a estrutura de request é muito parecida, vou colocar o request completo abaixo.

func getPokemon(id:Int, completion:@escaping (_ response: PokemonResponse) -> Void)
{

alamofireManager.request("\(PokemonAPIURL.Main)\(id)/", method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON
{ (response) in

let statusCode = response.response?.statusCode
switch response.result
{
case .success(let value):
let resultValue = value as? [String: Any]
if statusCode == 404
{
if let description = resultValue?["detail"] as? String
{
let error = ServerError(description: description, errorCode: statusCode!)
completion(.serverError(description: error))
}
}
else if statusCode == 200
{
//Mando para o parse dos pokemons
let model = self.parse.parsePokemon(response: resultValue)
completion(.success(model: model))
}
case .failure(let error):
let errorCode = error._code
if errorCode == -1009
{
let erro = ServerError(description: error.localizedDescription, errorCode: errorCode)
completion(.noConnection(description: erro))
}
else if errorCode == -1001
{
let erro = ServerError(description: error.localizedDescription, errorCode: errorCode)
completion(.timeOut(description: erro))
}
}
}
}

Nesse último request poucas coisas mudaram. Faço uma verificação se o id do pokemon é diferente de nulo e a função de parse é diferente.

Esse último request nos dá informações mais especificas de cada pokemon, como a sua imagem, por exemplo. Como ela vem em formato URL precisamos executar mais um request, para baixar esta imagem.

Lembro mais uma vez que a forma correta de se baixar imagens é usando o Alamofire/Image, porém não vou usa-lo nesse post.

Segue o request de imagem abaixo.

func getImagePokemon(url:String, completion:@escaping (_ response: ImageResponse) -> Void)
{
//O Evento responseData vai devoler um objeto Data case tenha sucesso
alamofireManager.request(url, method: .get).responseData
{ (response) in
if response.response?.statusCode == 200
{
//Se o obejto Data view nulo mesmo retornando 200 algum erro aconteceu
guard let data = response.data else
{
let erro = ServerError(description: "Falha no Download, data vazio", errorCode: 404)
completion(.serverError(description: erro))
return
}
//Retorno a imagem
completion(.success(model: data))
}
else
{
let erro = ServerError(description: "Falha no Download, data vazio", errorCode: 404)
completion(.serverError(description: erro))
}
}
}

Pois bem, terminamos todos os request, sabe o que falta? Testar!

Precisamos saber se nossos requests estão funcionando.

Testando

Para realizarmos o teste vamos modificar nosso arquivo ViewController.swift, criando 3 funções, uma para cada request e dando print em seus resultados.

Primeiro vamos criar uma váriavel da classe Request.

//Variavel responsvel por realizar todos os request
let request = ResquestPokedex()

Agora vamos criar a função que executa o request da Pokedex

func showPokedex()
{
//Passando nil como URL ele vai pegar a URL padrão
request.getAllPokemons(url: nil)
{ (response) in
switch response
{
case .success(let model):
//Print do model
print("X GET ALL POKEMONS \(model) \n")
//Chamo a proxima função para exibir um pokemon especifico
self.showPokemons()
case .serverError(let description):
print("Server Erro \(description)")
case .noConnection(let description):
print("No Connection \(description)")
case .timeOut(let description):
print("Time Out \(description)")
}
}
}

Próxima etapa é criar a função showPokemons usada no código acima.

func showPokemons()
{
//Um id é passado para que busque um pokemon especifico
request.getPokemon(id: 1)
{ (response) in
switch response
{
case .success(let model):
//Print do model do pokemon
print("Y GET POKEMON \(model) \n")
//Chamo a proxima função para exibir a imagem
self.showImagePokemon(urlImage: model.urlImage)
case .serverError(let description):
print("Server Erro \(description)")
case .noConnection(let description):
print("No Connection \(description)")
case .timeOut(let description):
print("Time Out \(description)")
}
}
}

Ultima função é a que exibe a imagem usada no código anterior.

func showImagePokemon(urlImage:String)
{
//Passa a url da imagem
request.getImagePokemon(url: urlImage)
{ (response) in
switch response
{
case .success(let model):
//Print do model da imagem
print("Z IMAGE POKEMON \(model)")
case .serverError(let description):
print("Server Erro \(description)")
case .noConnection(let description):
print("No Connection \(description)")
case .timeOut(let description):
print("Time Out \(description)")
}
}
}

Agora no viewDidLoad faça a chamada para a primeira função.

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
showPokedex()
}

De um Build and Run(Command+R) e veja a mágica acontecer no seu console.

Print da Pokedex
Print do Pokemon
Print da Imagem

No próximo episódio

No próximo post vamos ver como carregar essas informações visualmente em uma UITableViewController e finalizar a nossa querida Pokedex.

Caso tenha gostado do texto, recomende-o clicando no coração ali em baixo. Obrigado e até a próxima.

--

--