Cómo hacer una llamada a la API en Swift

Si está buscando convertirse en un desarrollador de iOS, hay algunas habilidades fundamentales que vale la pena conocer. Primero, es importante estar familiarizado con la creación de vistas de tabla. En segundo lugar, debe saber cómo llenar esas vistas de tabla con datos. En tercer lugar, es genial si puede obtener datos de una API y usar estos datos en la vista de su tabla.

El tercer punto es lo que cubriremos en este artículo. Desde la introducción de CodableSwift 4, hacer llamadas a la API es mucho más fácil. Anteriormente, la mayoría de la gente usaba pods como Alamofire y SwiftyJson (puede leer sobre cómo hacerlo aquí). Ahora, el método Swift es mucho mejor listo para usar, por lo que no hay razón para descargar un pod.

Repasemos algunos componentes básicos que se utilizan a menudo para realizar una llamada a la API. En primer lugar, cubriremos estos conceptos, ya que son partes importantes para comprender cómo realizar una llamada a la API.

  • Controladores de finalización
  • URLSession
  • DispatchQueue
  • Conservar ciclos

Finalmente lo juntaremos todo. Usaré la API de Star Wars de código abierto para construir este proyecto. Puedes ver mi código de proyecto completo en GitHub.

Alerta de descargo de responsabilidad: Soy nuevo en la codificación y en gran parte soy autodidacta. Disculpas si tergiversar algunos conceptos.

Controladores de finalización

¿Recuerdas el episodio de Friends donde Pheobe está pegada al teléfono durante días esperando para hablar con el servicio al cliente? Imagínese si al comienzo de esa llamada telefónica, una persona encantadora llamada Pip dijera: "Gracias por llamar. No tengo idea de cuánto tiempo tendrás que esperar en espera, pero te llamaré cuando estemos listos para ti." No habría sido tan divertido, pero Pip se ofrece a ser un controlador de finalización para Pheobe.

Utiliza un controlador de finalización en una función cuando sabe que la función tardará un poco en completarse. No sabes cuánto tiempo y no quieres hacer una pausa en tu vida esperando que termine. Entonces le pides a Pip que te toque en el hombro cuando esté lista para darte la respuesta. De esa manera, puede seguir con su vida, hacer algunos recados, leer un libro y mirar televisión. Cuando Pip te toque en el hombro con la respuesta, puedes tomar su respuesta y usarla.

Esto es lo que sucede con las llamadas a la API. Envía una solicitud de URL a un servidor, solicitándole algunos datos. Espera que el servidor devuelva los datos rápidamente, pero no sabe cuánto tiempo llevará. En lugar de hacer que su usuario espere pacientemente a que el servidor le proporcione los datos, utiliza un controlador de finalización. Esto significa que puede decirle a su aplicación que se apague y haga otras cosas, como cargar el resto de la página.

Dile al controlador de finalización que toque su aplicación en el hombro una vez que tenga la información que desea. Puede especificar cuál es esa información. De esa manera, cuando su aplicación recibe un toque en el hombro, puede tomar la información del controlador de finalización y hacer algo con ella. Por lo general, lo que hará es volver a cargar la vista de tabla para que el usuario vea los datos.

A continuación, se muestra un ejemplo de cómo se ve un controlador de finalización. El primer ejemplo es configurar la propia llamada API:

func fetchFilms(completionHandler: @escaping ([Film]) -> Void) { // Setup the variable lotsOfFilms var lotsOfFilms: [Film] // Call the API with some code // Using data from the API, assign a value to lotsOfFilms // Give the completion handler the variable, lotsOfFilms completionHandler(lotsOfFilms) }

Ahora queremos llamar a la función fetchFilms. Algunas cosas a tener en cuenta:

  • No necesita hacer referencia completionHandlercuando llama a la función. La única vez que hace referencia completionHandleres dentro de la declaración de función.
  • El controlador de finalización nos devolverá algunos datos para usar. Según la función que hemos escrito anteriormente, sabemos que podemos esperar datos de tipo [Film]. Necesitamos nombrar los datos para que podamos hacer referencia a ellos. Debajo estoy usando el nombre films, pero podría ser randomData, o cualquier otro nombre de variable que me gustaría.

El código se verá así:

fetchFilms() { (films) in // Do something with the data the completion handler returns print(films) }

URLSession

URLSessiones como el gerente de un equipo. El gerente no hace nada por su cuenta. Su trabajo es compartir el trabajo con las personas de su equipo, y ellos harán el trabajo. Su equipo lo es dataTasks. Cada vez que necesite algunos datos, escriba al jefe y utilícelo URLSession.shared.dataTask.  

Puede brindar los dataTaskdiferentes tipos de información para ayudarlo a lograr su objetivo. Dar información a dataTaskse llama inicialización. Inicializo mi dataTaskscon URL. dataTaskstambién utilizan controladores de finalización como parte de su inicialización. He aquí un ejemplo:

let url = URL(string: "//www.swapi.co/api/films") let task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in // your code here }) task.resume()

dataTasksutilizar los manipuladores de finalización, y siempre devuelven los mismos tipos de información: data, responsey error. Puede dar a estos tipos de datos diferentes nombres, como (data, res, err)o (someData, someResponse, someError). Por el bien de la convención, es mejor ceñirse a algo obvio en lugar de volverse pícaro con nuevos nombres de variables.

Empecemos por error. Si el dataTaskresultado es un error, querrá saberlo por adelantado. Significa que puede dirigir su código para manejar el error con elegancia. También significa que no se molestará en intentar leer los datos y hacer algo con ellos, ya que hay un error al devolver los datos.

A continuación, estoy manejando el error simplemente imprimiendo un error en la consola y saliendo de la función. Hay muchas otras formas en que podría manejar el error si quisiera. Piense en lo fundamentales que son estos datos para su aplicación. Por ejemplo, si tiene una aplicación bancaria y esta llamada a la API muestra a los usuarios su saldo, es posible que desee manejar el error presentando un modal al usuario que diga: "Lo sentimos, estamos experimentando un problema en este momento. Intente de nuevo más tarde."

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

A continuación, miramos la respuesta. Puede convertir la respuesta en un httpResponse. De esa manera, puede ver los códigos de estado y tomar algunas decisiones basadas en el código. Por ejemplo, si el código de estado es 404, entonces sabrá que no se encontró la página.

El siguiente código usa un guardpara verificar que existen dos cosas. Si ambos existen, entonces permite que el código continúe con la siguiente declaración después de la guardcláusula. Si alguna de las declaraciones falla, salimos de la función. Este es un caso de uso típico de una guardcláusula. Espera que el código después de una cláusula de protección sea el flujo de días felices (es decir, el flujo fácil sin errores).

 guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { print("Error with the response, unexpected status code: \(response)") return }

Finalmente, maneja los datos en sí. Tenga en cuenta que no hemos utilizado el controlador de finalización para erroro el response. Eso es porque el controlador de finalización está esperando datos de la API. Si no llega a la parte de datos del código, no es necesario invocar al controlador.

Para los datos, estamos usando JSONDecoderpara analizar los datos de una manera agradable. Esto es bastante ingenioso, pero requiere que haya establecido un modelo. Nuestro modelo se llama FilmSummary. Si JSONDecoderes nuevo para usted, eche un vistazo en línea para saber cómo usarlo y cómo usarlo Codable. Es realmente simple en Swift 4 y superior en comparación con Swift 3 días.

En el siguiente código, primero verificamos que existan los datos. Estamos bastante seguros de que debería existir, porque no hay errores ni respuestas HTTP extrañas. En segundo lugar, comprobamos que podemos analizar los datos que recibimos de la forma que esperamos. Si podemos, devolvemos el resumen de la película al controlador de finalización. En caso de que no haya datos para devolver de la API, tenemos un plan alternativo de la matriz vacía.

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

Entonces, el código completo para la llamada a la API se ve así:

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("Error with fetching films: \(error)") return } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { print("Error with the response, unexpected status code: \(response)") return } if let data = data, let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) { completionHandler(filmSummary.results ?? []) } }) task.resume() }

Conservar ciclos

NB: ¡Soy extremadamente nuevo en entender los ciclos de retención! Aquí está la esencia de lo que investigué en línea.

Es importante comprender los ciclos de retención para la gestión de la memoria. Básicamente, desea que su aplicación limpie bits de memoria que ya no necesita. Supongo que esto hace que la aplicación sea más eficiente.

Hay muchas formas en que Swift te ayuda a hacer esto automáticamente. Sin embargo, hay muchas formas en que puede codificar accidentalmente retener ciclos en su aplicación. Un ciclo de retención significa que su aplicación siempre conservará la memoria para una determinada pieza de código. Generalmente sucede cuando tienes dos cosas que tienen fuertes indicadores entre sí.

Para evitar esto, la gente suele utilizar weak. Cuando un lado del código es weak, no tiene un ciclo de retención y su aplicación podrá liberar la memoria.

Para nuestro propósito, un patrón común es usar [weak self]al llamar a la API. Esto asegura que una vez que el controlador de finalización devuelva algún código, la aplicación pueda liberar la memoria.

fetchFilms { [weak self] (films) in // code in here }

DispatchQueue

Xcode uses different threads to execute code in parallel. The advantage of multiple threads means you aren't stuck waiting on one thing to finish before you can move on to the next. Hopefully you can start to see the links to completion handlers here.

These threads seem to be also called dispatch queues. API calls are handled on one queue, typically a queue in the background. Once you have the data from your API call, most likely you'll want to show that data to the user. That means you'll want to refresh your table view.

Table views are part of the UI, and all UI manipulations should be done in the main dispatch queue. This means somewhere in your view controller file, usually as part of the viewDidLoad function, you should have a bit of code that tells your table view to refresh.

We only want the table view to refresh once it has some new data from the API. This means we'll use a completion handler to tap us on the shoulder and tell us when that API call is finished. We'll wait until that tap before we refresh the table.

The code will look something like:

fetchFilms { [weak self] (films) in self.films = films // Reload the table view using the main dispatch queue DispatchQueue.main.async { tableView.reloadData() } }

viewDidLoad vs viewDidAppear

Finally you need to decide where to call your fetchfilms function. It will be inside a view controller that will use the data from the API. There are two obvious places you could make this API call. One is inside viewDidLoad and the other is inside viewDidAppear.

These are two different states for your app. My understanding is viewDidLoad is called the first time you load up that view in the foreground. viewDidAppear is called every time you come back to that view, for example when you press the back button to come back to the view.

If you expect your data to change in between the times that the user will navigate to and from that view, then you may want to put your API call in viewDidAppear. However I think for almost all apps, viewDidLoad is sufficient. Apple recommends viewDidAppear for all API calls, but that seems like overkill. I imagine it would make your app less performant as it's making many more API calls that it needs to.

Combining all the steps

First: write the function that calls the API. Above, this is fetchFilms. This will have a completion handler, which will return the data you are interested in. In my example, the completion handler returns an array of films.

Second: call this function in your view controller. You do this here because you want to update the view based on the data from the API. In my example, I am refreshing a table view once the API returns the data.

Third: decide where in your view controller you would like to call the function. In my example, I call it in viewDidLoad.

Fourth: decide what to do with the data from the API. In my example, I am refreshing a table view.

Inside NetworkManager.swift (this function can be defined in your view controller if you'd like, but I am using the MVVM pattern).

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("Error with fetching films: \(error)") return } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { print("Error with the response, unexpected status code: \(response)") return } if let data = data, let filmSummary = try? JSONDecoder().decode(FilmSummary.self, from: data) { completionHandler(filmSummary.results ?? []) } }) task.resume() }

Inside 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() } } } // other code for the view controller }

Gosh, we made it! Thanks for sticking with me.