Artigo original: How to make an API call in Swift

Se você deseja se tornar um desenvolvedor iOS, existem algumas habilidades fundamentais que vale a pena conhecer. Em primeiro lugar, é importante estar familiarizado com a criação de visualizações de tabela. Em segundo, você deve saber como preencher essas visualizações de tabela com dados. Em terceiro, é ótimo se você puder buscar dados de uma API e usar esses dados em sua visualização de tabela.

O terceiro ponto é o que abordaremos neste artigo. Desde a introdução do Codable no Swift 4, fazer chamadas de API ficou muito mais fácil. Anteriormente, a maioria das pessoas usava pods como Alamofire e SwiftyJson (você pode ler sobre como fazer isso aqui - texto em inglês). Agora, o Swift está muito melhor desde o início. Assim, não há motivo para baixar um pod.

Vamos passar por alguns blocos de construção que são frequentemente usados para fazer uma chamada de API. Abordaremos esses conceitos primeiro, pois eles são partes importantes para entender como fazer uma chamada de API.

  • Manipuladores de conclusão (em inglês, Completion handlers)
  • URLSession
  • DispatchQueue
  • Ciclos de retenção

Por fim, vamos juntar isso tudo. Usaremos a API de Star Wars de código aberto para construir esse projeto. Você pode ver o código completo do meu projeto no GitHub.

Alerta de isenção de responsabilidade: sou nova na programação e sou autodidata. Peço desculpas se deturpei alguns conceitos.

Manipuladores de conclusão

pheobe
Pobre e paciente Phoebe

Lembra do episódio de Friends em que a Phoebe fica grudada no telefone por dias esperando para falar com o atendimento ao cliente? Imagine se, bem no início dessa ligação, uma pessoa adorável chamada Pip dissesse: "Obrigado por ligar. Não tenho ideia de quanto tempo você precisará esperar, mas ligo de volta quando estivermos prontos para você." Não teria sido tão engraçado, mas Pip estaria se oferecendo para ser um manipulador de conclusão para Phoebe.

Você usa um manipulador de conclusão em uma função quando sabe que essa função demorará um pouco para ser concluída. Você não sabe quanto tempo e não quer pausar sua vida esperando que ela termine. Então, você pede a Pip para dar um toque a você quando ela estiver pronta para dar uma resposta. Desse modo, você pode seguir sua vida, sair para comprar coisas, ler um livro e assistir TV. Quando Pip avisar você que já tem a resposta, você pode pegá-la e usá-la.

Isso é o que acontece com as chamadas de API. Você envia uma solicitação de URL para um servidor, solicitando alguns dados. Você espera que o servidor retorne os dados rapidamente, mas não sabe quanto tempo levará. Em vez de fazer seu usuário esperar pacientemente até que o servidor forneça os dados, você usa um manipulador de conclusão. Isso significa que você pode dizer ao seu aplicativo para desligar e fazer outras coisas, como carregar o resto da página.

Você diz ao manipulador de conclusão para avisar o seu aplicativo assim que ele tiver as informações desejadas. Você pode especificar quais são essas informações. Desse modo, quando seu aplicativo é avisado, ele pode pegar as informações do manipulador de conclusão e fazer algo com elas. Normalmente, o que você fará é recarregar a visualização da tabela para que os dados apareçam para o usuário.

Aqui está um exemplo da aparência de um manipulador de conclusão. O primeiro exemplo é de quando configuramos a própria chamada da API:

func fetchFilms(completionHandler: @escaping ([Film]) -> Void) {
  // Configurar a variável lotsOfFilms
  var lotsOfFilms: [Film]
  
  // Chamar a API com algum código
  
  // Usar os dados da API, atribuir um valor a lotsOfFilms  
  
  // Dar ao manipulador de conclusão a variável lotsOfFilms
  completionHandler(lotsOfFilms)
}
A function that uses a completion handler

Agora, podemos invocar a função fetchFilms. Algumas coisas a serem observadas:

  • Você não precisa referenciar o completionHandler quando você chama a função. A única vez em que você faz referência ao completionHandler está dentro da declaração da função.
  • O manipulador de conclusão nos devolverá alguns dados para usar. Com base na função que escrevemos acima, sabemos esperar dados do tipo [Film]. Precisamos nomear os dados para que possamos nos referir a eles. Abaixo, estou usando o nome films, mas poderia ser randomData, ou qualquer outro nome de variável.

O código ficará mais ou menos assim:

fetchFilms() { (films) in
  // Fazer algo com os dados retornados pelo manipulador de conclusão 
  print(films)
}
Implementing a function with a completion handler

URLSession

URLSession é como o gerente de uma equipe. O gerente não faz nada sozinho. Seu trabalho é compartilhar o trabalho com as pessoas de sua equipe, e eles farão o trabalho. A equipe dela são as dataTasks. Toda vez que você precisar de alguns dados, escreva para o chefe e use URLSession.shared.dataTask.

Você pode dar ao dataTask diferentes tipos de informações para ajudá-lo a atingir seu objetivo. Dar informações ao dataTask é chamado de inicialização. Eu inicializo meu dataTasks com URLs. As dataTasks também usam manipuladores de conclusão como parte de sua inicialização. Aqui está um exemplo:

let url = URL(string: "https://www.swapi.co/api/films")

let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in 
  // Insira seu código aqui
})

task.resume()
How to use URLSession to fetch some data

As dataTasks usam manipuladores de conclusão e sempre retornam os mesmos tipos de informações: data, response e error. Você pode dar nomes diferentes a esses tipos de dados, como(data, res, err) ou (someData, someResponse, someError). Por uma questão de convenção, é melhor se ater a algo óbvio em vez de usar novos nomes de variáveis.

Vamos começar com error. Se dataTask retorna um error, você vai querer saber isso antecipadamente. Isso significa que você pode direcionar seu código para lidar com o erro normalmente. Isso também significa que você não se incomodará em tentar ler os dados e fazer algo com eles, pois há um erro ao retornar os dados.

Abaixo, estou lidando com o erro simplesmente imprimindo um erro no console e saindo da função. Existem muitas outras maneiras de lidar com o erro, se desejar. Pense em como esses dados são fundamentais para seu aplicativo. Por exemplo, se você tiver um aplicativo bancário e essa chamada de API mostrar o saldo dos usuários, convém lidar com o erro apresentando um modal ao usuário que diz: "Desculpe, estamos enfrentando um problema agora. Tente de novo mais tarde."

if let error = error {
  print("Error accessing swapi.co: /(error)")
  return
}
Tratamento do erro

Em seguida, analisamos a resposta. Você pode converter a resposta para que seja um httpResponse. Desse modo, você pode ver os códigos de status e tomar algumas decisões com base no código. Por exemplo, se o código de status for 404, você saberá que a página não foi encontrada.

O código abaixo usa guard para verificar se existem duas coisas. Se ambos existirem, ele permitirá que o código continue para a próxima instrução após a cláusula de guard. Se qualquer uma das instruções falhar, saímos da função. Este é um caso de uso típico de uma cláusula de guard. Você espera que o código após uma cláusula de guard seja o fluxo ideal (ou seja, o fluxo fácil sem erros).

  guard let httpResponse = response as? HTTPURLResponse,
            (200...299).contains(httpResponse.statusCode) else {
    print("Erro na resposta. Código de status esperado: \(response)")
    return
  }

Finalmente, você lida com os dados em si. Observe que não usamos o manipulador de conclusão para error ou para response. Isso ocorre porque o manipulador de conclusão está aguardando dados da API. Se não chegar à parte de dados do código, não há necessidade de invocar o manipulador.

Estamos usando o JSONDecoder para analisar os dados de uma maneira agradável. Isso é muito bom, mas requer que você tenha estabelecido um modelo. Nosso modelo é chamado FilmSummary. Se o JSONDecoder for novo para você, dê uma olhada on-line para saber como usá-lo e como usar o Codable. É muito simples no Swift 4 e em versões superiores em comparação com a época do Swift 3.

No código abaixo, primeiro verificamos se os dados existem. Temos certeza de que deve existir, porque não há erros nem respostas HTTP estranhas. Em segundo lugar, verificamos se podemos analisar os dados que recebemos da maneira que esperamos. Se pudermos, devolvemos o resumo do filme ao manipulador de conclusão. Caso não haja dados para retornar da API, temos um plano de retorno (em inglês, fall back) do array vazio.

if let data = data,
        let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) {
        completionHandler(filmSummary.results ?? [])
      }

Portanto, o código completo para a chamada da API fica assim:

func fetchFilms(completionHandler: @escaping ([Film]) -> Void) {
    let url = URL(string: domainUrlString + "films/")!

    let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
      if let error = error {
        print("Erro ao obter os filmes: \(error)")
        return
      }
      
      guard let httpResponse = response as? HTTPURLResponse,
            (200...299).contains(httpResponse.statusCode) else {
        print("Erro na resposta. Código de status esperado: \(response)")
        return
      }

      if let data = data,
        let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) {
        completionHandler(filmSummary.results ?? [])
      }
    })
    task.resume()
  }

Ciclos de retenção

Aviso: eu estou muito no início dos meus estudos nova para entender os ciclos de retenção! Aqui está a essência do que eu pesquisei on-line.

Os ciclos de retenção são importantes para o gerenciamento de memória. Basicamente, você quer que seu aplicativo limpe pedaços de memória de que não precisa mais. Suponho que isso torna o aplicativo mais eficiente.
Há muitas maneiras de o Swift ajudar você a fazer isso automaticamente.

No entanto, há muitas maneiras de codificar acidentalmente ciclos de retenção em seu aplicativo. Um ciclo de retenção significa que seu aplicativo sempre manterá a memória para um determinado trecho de código. Geralmente, isso acontece quando você tem duas coisas que têm indicadores fortes uma para a outra.

Para contornar isso, as pessoas costumam usar weak. Quando um lado do código é weak, você não tem um ciclo de retenção e seu aplicativo poderá liberar a memória.

Para nosso propósito, um padrão comum é usar [weak self] ao chamar a API. Isso garante que, assim que o manipulador de conclusão retornar algum código, o aplicativo possa liberar a memória.

fetchFilms { [weak self] (films) in
  // Seu código aqui
}

Fila de despacho (DispatchQueue, em inglês)

O Xcode usa diferentes threads para executar código em paralelo. A vantagem de vários threads está no fato de que você não fica parado esperando que uma coisa termine antes de passar para a próxima. Espero que você já esteja vendo a ligação com os manipuladores de conclusão aqui.

Esses threads também parecem ser chamados de filas de despacho. As chamadas de API são tratadas em uma fila – normalmente, uma fila em segundo plano. Depois de ter os dados de sua chamada de API, provavelmente você desejará mostrar esses dados ao usuário. Isso significa que você desejará atualizar sua visualização de tabela.

As exibições de tabela fazem parte da interface do usuário. Todas as manipulações da interface do usuário devem ser feitas na fila de despacho principal. Isso significa que, em algum lugar no seu arquivo de controlador de visualização, geralmente, como parte da função viewDidLoad, você deve ter um trecho de código que diga à sua visualização de tabela para atualizar.

Queremos que a visualização de tabela seja atualizada apenas quando tiver alguns novos dados da API. Isso significa que usaremos um manipulador de conclusão para nos avisar quando a chamada da API for concluída. Vamos esperar até esse aviso antes de atualizar a tabela.

O código será algo como:

fetchFilms { [weak self] (films) in
  self.films = films

  // Recarregar a visualização de tabela usando a fila de despacho principal
  DispatchQueue.main.async {
    tableView.reloadData()
  }
}

viewDidLoad x viewDidAppear

Finalmente, você precisa decidir onde chamar sua função fetchfilms. Ele estará dentro de um controlador (em inglês, controller) de visualização, que usará os dados da API. Existem dois lugares óbvios em que você pode fazer essa chamada de API. Um está dentro de viewDidLoad e o outro está dentro de viewDidAppear.

Esses são dois estados diferentes para seu aplicativo. Meu entendimento é o de que viewDidLoad é chamado na primeira vez em que você carrega essa visualização em primeiro plano. viewDidAppear é chamado toda vez que você volta a essa visualização, por exemplo, quando você pressiona o botão Voltar para voltar à visualização.

Se você espera que seus dados mudem entre os momentos em que o usuário navegará para e a partir dessa visualização, convém colocar sua chamada de API em viewDidAppear. No entanto, acho que para quase todos os aplicativos viewDidLoad é suficiente. A Apple recomenda viewDidAppear para todas as chamadas de API, mas isso parece um exagero. Imagino que isso tornaria seu aplicativo menos eficiente, pois está fazendo muito mais chamadas de API do que precisa.

Combinando todas as etapas

Primeiro: escreva a função que chama a API. Acima, essa função é a fetchFilms. Ela terá um manipulador de conclusão, que retornará os dados nos quais você está interessado. No meu exemplo, o manipulador de conclusão retorna um array de filmes.

Segundo: chame essa função em seu controlador de visualização. Você faz isso aqui porque deseja atualizar a visualização com base nos dados da API. No meu exemplo, estou atualizando uma visualização de tabela assim que a API retornar os dados.

Terceiro: decida onde em seu controlador de visualização você gostaria de chamar a função. No meu exemplo, eu chamo de viewDidLoad.

Quarto: decida o que fazer com os dados da API. No meu exemplo, estou atualizando uma visualização de tabela.

Dentro de NetworkManager.swift (esta função pode ser definida em seu controlador de visualização se você quiser, mas estou usando o padrão MVVM).

func fetchFilms(completionHandler: @escaping ([Film]) -> Void) {
    let url = URL(string: domainUrlString + "films/")!

    let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
      if let error = error {
        print("Erro ao obter os filmes: \(error)")
        return
      }
      
      guard let httpResponse = response as? HTTPURLResponse,
            (200...299).contains(httpResponse.statusCode) else {
        print("Erro na responta. Código de status inesperado: \(response)")
        return
      }

      if let data = data,
        let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) {
        completionHandler(filmSummary.results ?? [])
      }
    })
    task.resume()
  }

Dentro de FilmsViewController.swift:

final class FilmsViewController: UIViewController {
  private var films: [Film]?

  override func viewDidLoad() {
    super.viewDidLoad()
    
    NetworkManager().fetchFilms { [weak self] (films) in
      self?.films = films
      DispatchQueue.main.async {
        self?.tableView.reloadData()
      }
    }
  }
  
  // o resto do código para o controlador de visualizações
}

Puxa, conseguimos! Obrigado por ficar comigo até o final da leitura.

Se essa publicação foi útil para você, compartilhe! Obrigada!