Async/Await: Camada Network genérica com Swift 5.5

Victor Catão
10 min readMar 10, 2022
Photo by Taylor Vick on Unsplash.

Receber uma response de um servidor é uma das partes mais legais de quando está aprendendo a desenvolver uma aplicação front-end. É nessa interação que o app começa a ganhar vida.

Esse artigo vai te mostrar como fazer uma camada de Network simples para o seu app de forma genérica, sem que você precise ficar escrevendo um monte de código toda hora que precisar fazer uma request, ou seja, evitando boilerplate.

Caso não domine ainda o conceito de API, recomendo que antes de dar continuidade leia este artigo. Além disso, é importante também ter uma boa noção sobre Protocol Oriented Programming (POP) e este artigo sobre async/await.

TMDB: The Movie Database

Para nos auxiliar neste artigo, iremos utilizar a API do TMDB para realizar algumas requests, as quais você pode ver a documentação clicando aqui. De forma sucinta, o TMDB é um banco de dados que contém diversas informações sobre filmes, TV, séries, etc.

O uso da API é gratuita, porém isso não significa que ela seja completamente aberta. Para fazer as requisições você precisará criar uma conta para ter o seu Access Token. Assim:

1- Acesse o site do The Movie Database: https://www.themoviedb.org/

2- Clique em “Join TMDB”

3- Faça o seu cadastro e depois faça o login.

4- Vá até Settings > API: https://www.themoviedb.org/settings/api

Pronto. No final da página você vai encontrar o seu token na seção API Read Access Token (v4 auth). É algo assim:

TMDB Access Token.

Projeto

Para que você possa seguir o artigo, criei este projeto de exemplo que está utilizando a arquitetura MVC. Você pode clonar o repositório e abrir o .xcodeproject para acompanhar, onde você encontrará estas três pastas onde estão localizadas os principais arquivos que iremos abordar neste primeiro momento.

Pastas do projeto.
  • Endpoint: Um protocol de configuração de todos os endpoints.
  • HTTPClient: Um dos arquivos mais importantes para este artigo. É nele que se encontra o método que executa as requisições genéricas.
  • HTTPMethod: Enum com os principais métodos HTTP para realizar as request. Se necessário, você pode adicionar outros, como head, trace, options, etc.
  • RequestError: Enum com alguns erros que gerenciaremos. Caso a API que você consuma ou o seu app tenha outros casos particulares, adicione-os neste enum.
  • MoviesEndpoint: Enum com os endpoints do serviço Movies da API do TMDB. Para cada case é possível configurar o endpoint específico, determinando as variáveis path, method, header e body.
  • MoviesService: Struct responsável para realizar as requests do serviço Movies.
  • TopRated: Model para fazer o decode da response do endpoint top_rated.
  • Movie: Model para fazer o decode das responses que contém filme(s).
  • MainViewController: A única Controller do projeto, responsável por fazer a requisição e mostrar a response.
Projeto de exemplo.

Camada de Network

Antes de chegar no mais legal do artigo (HTTPClient) precisamos passar por outras partes para que você possa entender tudo o que está acontecendo.

O Endpoint é um protocol onde todos os endpoints precisam conformar para informar todos os seus detalhes: scheme, host, path, method, header e body.

Ele possui uma implementação default das variáveis scheme e host pois normalmente são iguais para todos os endpoints e, portanto, não é necessário implementar a todo momento. Se necessário, é possível fazer um override dessa implementação.

A variávelpath será utilizada como complemento da scheme e host para formar a URL do endpoint. Funciona assim: scheme + host + path. Por exemplo: suponha que a URL do endpoint seja https://api.themoviedb.org/3/movie/top_rated, então o path deve ser /3/movie/top_rated.

A variável header deve retornar um dicionário com todos as informações de header que a documentação do endpoint pedir, quando necessário. Normalmente é nele que se faz a autenticação por meio do Authorization, que você verá a seguir.

Assim como a variável header, quando necessário, o body também é um dicionário que deve conter as informações do body que a documentação do endpoint pedir para que sejam enviadas.

A variável method é do tipo RequestMethod, que representa o método HTTP do endpoint, podendo ser: GET, POST, PUT, DELETE, PATCH, HEAD, etc. Para esta API foram colocados apenas os 5 principais métodos, mas você pode adicionar mais caso faça sentido no seu contexto.

O enum RequestError contém alguns possíveis erros que o HTTPClient irá identificar e retornar para que possa ser gerenciado pelo app.

Ele possui a variável customMessage, que pode ser utilizada para exibir uma mensagem de feedback para o usuário ou para track de erros, por exemplo.

A struct TopRated é um model Codable para realizar o decode da response do endpoint top_rated da API do TMDB. Observe que ela possui o enum CodingKeys que foi utilizado para renomear as entradas Snake case (enviadas pela API, como “total_pages”) para Camel case, que é o padrão mais utilizado em swift. A struct Movie segue a mesma ideia.

Bom, agora que você já conhece todas as partes necessárias para se entender o HTTPClient, finalmente chegou a parte mais importante deste artigo.

HTTPClient é um protocol com uma implementação default da funçãosendRequest, responsável por realizar a request para o servidor. Como parâmetro ela recebe o Endpoint e o tipo do model a ser realizado o decode da response da API. Já como retorno ele lança um Result que pode ser um success com a response decodificada ou então um error que com um dos cases criados no RequestError. E como você pode perceber, ela está acompanhada da keyword async, que representa que é uma função assíncrona que irá rodar em uma thread separada e pode lançar o Result a qualquer momento.

Como foi dito, o protocol Endpoint contém todas as informações necessárias para consumi-lo. É aqui que todas essas informações serão utilizadas.

O método inicia criando a URL a ser utilizada com o URLComponents. Aqui é onde usaremos as variáveis scheme, host e path para configurar o objeto urlComponents. Através dele vamos obter a URL para ser utilizada na instanciação da URLRequest e, caso falhe, o erro .invalidURL será retornado.

Depois isso, precisamos criar uma URLRequest para o consumo do endpoint. Mas pera aí, o que é essa URLRequest? A documentação da Apple irá nos ajudar:

URLRequest encapsulates two essential properties of a load request: the URL to load and the policies used to load it. In addition, for HTTP and HTTPS requests, URLRequest includes the HTTP method (GET, POST, and so on) and the HTTP headers.

Então o que o código faz em seguida é criar uma URLRequest a partir da URL instanciada. Depois disso faz-se a configuração do httpMethod e allHTTPHeaderFields através, respectivamente, das variáveis method e header do Endpoint passado por parâmetro.

O httpBody também é configurado seguindo a mesma lógica, caso haja algum dado a ser enviado para o endpoint.

Agora só falta realizar a request. Estamos perto! Para isso, utilizaremos a URLSession.shared.data com a nossa URLRequest que acabou de ser criada. Mas espera aí, o que é URLSession? De novo, vamos recorrer à documentação da Apple:

The URLSession class and related classes provide an API for downloading data from and uploading data to endpoints indicated by URLs.

Em outras palavras, é a classe que abstrai toda a comunicação entre o app e a API. É ela que, com todas as informações necessárias para a realização da request, nos retornará a response da API.

Na linha abaixo é criada a tupla de retorno da função URL.shared.data(). Perceba que é necessário utilizar um try pois uma exception pode ser lançada ao rodar esta função (é por este motivo que estamos usando um do/catch) e, além disso, também possui a keyword await, que fará com que a próxima linha seja executada apenas quando a função URL.shared.data() finalizar e retornar a tupla.

Agora o trabalho está praticamente finalizado. Se a API retornou alguma response, prosseguimos com a análise do statusCode e, caso contrário, retornamos o erro .noResponse do RequestError.

Dado que a API retornou uma response, a análise do statusCode é feita em cima dos possíveis status HTTP. Para este artigo foi feita uma triagem de um caso ideal, onde [200, 201, …, 299] seria considerado um success e portanto podemos tentar fazer o decode da response, transformando o JSON recebido em um model Decodable e finalmente retornando para que o responsável possa utilizá-lo.

O caso 401 (unauthorized), normalmente causado por uma sessão expirada, também foi considerado, a qual cada contexto exige uma ação do app (por exemplo: pedir para o usuário fazer o login novamente).

Uso

Agora que você já entendeu como funciona a camada de Network, é necessário entender como utilizá-la. É fácil, vamos lá!

Para todos os endpoints dentro do serviço Movies, nós utilizaremos o MoviesEndpoint.

O MoviesEndpoint é enum que conforma com o protocol Endpoint. Ele possui todas as informações necessárias para cada endpoint. Para adicionar um novo endpoint do serviço Movies, basta adicionar um novo case e completar as informações dentro de cada variável: path, method, header e body.

Finalmente vamos falar do Service, que é a struct responsável por realizar a request.

A MoviesService é a struct utilizada pelo app para fazer as requests. Repare aqui em alguns pontos importantes:

  • Ela conforma com o protocol HTTPClient, o que significa que ela possui internamente a função sendRequest para realizar as requests e, por isso, não precisa ficar repetindo código para cada request.
  • Ela conforma com o protocol MoviesServiceable, o que é de suma importância para a realização dos testes e da injeção de dependência. Você verá mais detalhes em breve.
  • A declaração das funções possuem as keywords async, seguindo a mesma lógica já comentada na HTTPClient, evidenciando que é uma função assíncrona.

Agora que você entendeu como as structs Service funcionam, basta ver como é a sua utilização na MainViewController.

O MoviesService que criamos será utilizado através de injeção de dependência de um objeto que conforme com o protocolo MoviesServiceable. A MainViewController foi instanciada assim:

Essa injeção de dependência poderia ser feita de outra forma, mas foi feita assim para não deixar o artigo muito extenso. Poderia ser utilizado, por exemplo, a arquitetura MVVM e injetado na ViewController um ViewModel que houvesse o service dentro.

Agora só falta de fato fazer a request utilizando o Service injetado. Como estamos utilizando uma função async, precisamos criar uma Task para rodar o código assíncrono. Veja a documentação da Apple referente à esta função:

Use this function when creating asynchronous work that operates on behalf of the synchronous function that calls it.

O await fará com que a linha do switch seja executada apenas quando a requisição retornar um Result<Movie, RequestError>. Ao retornar, é possível fazer o switch para gerenciá-lo da forma necessária dentro de cada contexto.

É isso! Agora vamos ver um pouco sobre como podemos testar nossa camada de Network e utilizar mocks.

Testes

Agora você só precisa saber como fazer testes com a sua camada de network e utilizar mocks. O projeto possui os 4 arquivos abaixos:

  • RequestAppTests: Classe responsável por realizar os testes do app.
  • Mockable: Um protocolo para converter arquivos JSON em Codable.
  • top_rated_response: Arquivo .json com um exemplo de retorno da API topRated do TMDB.
  • movie_response: Arquivo .json com um exemplo de retorno da API de detalhe do um filme do TMDB.

O Mockable é um protocolo que evita boilerplate com uma implementação default da função loadJSON, que lê um arquivo interno .json e converte em um determinado model Codable.

Nada demais, certo? Agora veja o código abaixo de como podemos utilizar o protocol Mockable e o MoviesServiceable para criar um mock.

Você se lembra que a nossa classe de Service utilizada no app (MoviesService) também conforma com o protocol MoviesServiceable? Você também se lembra que quando utilizamos a injeção de dependência na ViewController nós injetamos uma instância do tipo MoviesServiceable e não um MoviesService? Dessa forma você pode injetar uma outra instância diferente para poder reutilizar e testar a sua ViewController. Veja o caso do mock abaixo:

Como o função loadTableView é responsável por fazer a request e atualizar a variável movies, então o movies.count deve ser igual a quantidade de itens no meu top_rated_response.json, que no caso é apenas 1. Além disso, o código também testa se o título deste item é o mesmo título contido no arquivo JSON.

Também é possível você testar somente o seu mock, assim:

Existem diversas formas de implementar os testes. Por exemplo, poderíamos ter um ViewModel com o Service injetável para testar requests e diversas funções da responsabilidade do ViewModel e separadamente realizar testes de UI com a ViewController.

Para utilizar outros JSONs no retorno do mock poderíamos criar outras classes Mockables & MoviesServiceable com diferentes retornos, ou então tornar a classe MoviesServiceMock mais reutilizável, injetando nomes de arquivos, tipos de falhas, etc.

Também seria possível deixar a camada de network mais robusta com implementação de cache, multipart, gerenciando outros status codes, etc. Além disso, poderíamos injetar o próprio JSONDecoder e URLSession para tornar ainda mais reutilizável e testável.O artigo se baseou em um contexto simples, mas cada contexto exige uma implementação e complexidade específica.

Gostou deste artigo? Me ajude a continuar escrevendo compartilhando com seus amigos, seguindo aqui no Medium e deixando suas palmas 👏 ❤️

Você também pode me adicionar no LinkedIn ;)

Abraços!

--

--

Victor Catão

Sr. iOS Developer at Uber, traveler, passionate about technology 🇧🇷