Aprende a usar URLSession para crear un Publisher y así usar varios operadores como map, decode, tryMap, etc
Aprende a usar URLSession para crear un Publisher y así usar varios operadores como map, decode, tryMap, etc

Aprende a usar URLSession y Combine (decode, tryMap y map)

Podemos usar Combine con URLSession, de esta manera creamos un Publisher en el que podemos usar operadores y así transformar los valores que viajan en nuestro Publisher. En este caso utilizamos los operadores map, tryMap, y decode. Es un ejemplo muy práctico y real.

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
URLSession y Combine en Swift
URLSession y Combine en Swift

Hoy en SwiftBeta vamos a aprender a cómo crear una petición HTTP usando Combine. Vas a ver lo sencillo y limpio que es realizar la petición HTTP y luego usar varios operadores de Combine para transformar el JSON obtenido a un modelo de nuestro dominio.

A lo largo de los videos del canal, hemos aprendido a realizar una petición HTTP de varias maneras, usando completionBlocks, async/await, promesas como vimos en el último video de la serie de Combine, etc. En el video de hoy vamos a usar directamente un Publisher de URLSession.

Creamos una nueva page en nuestro Playground

Antes de empezar con el nuevo video, vamos a crear una nueva Page en nuestro Playground. Si acabas de llegar al canal, por cada page que tenemos en nuestro Playground es un video que encontrarás en la serie de Combine. En este caso creamos una page llamada URLSession.

Una vez creada la Page, lo primero de todo que vamos hacer es importar Combine.

Antes de continuar, si quieres apoyar el contenido que subo cada semana, suscríbete. De esta manera seguiré publicando contenido completamente grautito en Youtube.

Ahora sí, vamos a continuar, lo siguiente que vamos hacer es buscar una URL donde queramos hacer una petición HTTP. En nuestro caso vamos a escoger una API que ya hemos usado en varios videos del canal, la API de Rick and Morty.

Es una API muy sencilla, podemos ir al siguiente enlace para obtener más información del endpoint que vamos a usar https://rickandmortyapi.com

Una vez aquí nos vamos a documentación, y escogemos este endpoint https://rickandmortyapi.com/api, fíjate que este método nos retorna un JSON muy sencillo, 3 keys con 3 valores. Una vez sabemos el endpoint al que vamos a llamar, lo siguiente que vamos hacer es crear el modelo de nuestro dominio, de esta manera podremos transformar el JSON a una struct, y esta struct es la siguiente:

struct DataModel: Decodable {
    let characters: String
    let episodes: String
    let locations: String
}
Struct para transformar el JSON a un modelo de nuestro dominio

Perfecto, lo siguiente que vamos hacer es realizar la petición HTTP. Creamos un tipo URL con el endpoint que vamos a usar https://rickandmortyapi.com/api

let url = URL(string: "https://rickandmortyapi.com/api")!
Endpoint

Y ahora es donde viene la parte interesante, vamos a usar URLSession, y en este caso usamos el método llamado dataTaskPublisher. Al añadir este método podemos escoger entre 2 opciones, una pasándole una URL y la otra pasándole un URLRequest. Escogemos la primera:

let url = URL(string: "https://rickandmortyapi.com/api")!
URLSession.shared.dataTaskPublisher(for: url)
Método dataTaskPublisher de URLSession

Al utilizar esta última línea, podemos trabajar como si de un Publisher se tratara, podemos concatenar diferentes operadores y manipular nuestro Publisher como hemos visto en los anteriores videos.

En este caso, al realizar la petición HTTP, vamos a usar el operador tryMap para inspeccionar el resultado ¿Por qué quiero hacer esto?

  • En caso de obtener un status code que no sea igual a 200, voy a lanzar un error especifico. Este error lo quiero lanzar en mi código al hacer una comprobación que crearemos a en los próximos minutos.
  • Y por otro lado, si todo va bien, retornamos el data de la petición HTTP

Al usar el operador tryMap, dentro del closure recibimos un data y un response de nuestra petición HTTP. Vamos a comprobar que el status code de la petición es 200 para publicar el data dentro del Publisher, y sino publicamos un error en nuestro Publisher de tipo URLError(.badServerResponse):

let url = URL(string: "https://rickandmortyapi.com/api")!
URLSession.shared.dataTaskPublisher(for: url)
    .tryMap { (data, response) -> Data in
            guard let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {
                    throw URLError(.badServerResponse)
                }
            return data
            }
Operador tryMap para tener más granuralidad de nuestro Publisher

Si compilamos vemos que no obtenemos ningún error. Vamos a continuar. Si todo va bien y se publica el data dentro del Publisher, lo que vamos hacer es recogerlo usando otro operador llamado decode. Este operador nos va a servir para transforma el data a un modelo de nuestro dominio, y para ser más concretos a la Struct que hemos creado al inicio del video llamada DataModel:

let url = URL(string: "https://rickandmortyapi.com/api")!
URLSession.shared.dataTaskPublisher(for: url)
    .tryMap { (data, response) -> Data in
            guard let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {
                    throw URLError(.badServerResponse)
                }
            return data
            }
    .decode(type: DataModel.self, decoder: JSONDecoder())
Operador decode para transformar data al modelo de nuestro dominio DataModel

Si compilamos, vemos que seguimos sin tener errores, para ver que todo funciona correctamente, vamos a retener nuestro subscriber asignándolo a una constante llamada cancellable:

let cancellable = URLSession.shared.dataTaskPublisher(for: url)
    .tryMap { (data, response) -> Data in
            guard let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {
                    throw URLError(.badServerResponse)
                }
            return data
            }
    .decode(type: DataModel.self, decoder: JSONDecoder())
Retenemos nuestro subscriber

Y ahora nos vamos a suscribir a nuestro Publisher, y ¿cómo lo hacemos? Pues de la misma manera que hemos visto en todos los anteriores videos de la serie de Combine, vamos a usar el método sink:

let url = URL(string: "https://rickandmortyapi.com/api")!
let cancellable = URLSession.shared.dataTaskPublisher(for: url)
    .tryMap { (data, response) -> Data in
            guard let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {
                    throw URLError(.badServerResponse)
                }
            return data
            }
    .decode(type: DataModel.self, decoder: JSONDecoder())
    .sink { completion in
        switch completion {
        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        case .finished:
            print("Finished")
        }
    } receiveValue: { value in
        print("Value: \(value)")
    }
Utilizamos sink para suscribirnos a nuestro Publisher

Si ahora compilamos, vamos a ver el resultado que aparece por consola:

Value: DataModel(characters: "https://rickandmortyapi.com/api/character", episodes: "https://rickandmortyapi.com/api/episode", locations: "https://rickandmortyapi.com/api/location")
Finished
Resultado obtenido al realizar la petición HTTP, todo OK

Perfecto, la petición HTTP se ha realizado correctamente, al hacerlo obtenemos el data. Esta data se ha pasado al operador decode que ha transformado este valor en un objeto de nuestro dominio llamado DataModel. Y finalmente hemos obtenido este valor en el subscriber.

¿Qué ocurriría si hay un error al realizar la petición HTTP? Vamos a probarlo cambiando la url por una que no exista, vamos a usar la siguiente url

let url = URL(string: "https://rickandmortyapi.com/ap")!
Modificamos el endpoint para obtener un error al realizar la petición HTTP

Si ahora compilamos, pulsamos COMMAND+SHIFT+ENTER, vemos por consola que al subscriber le llega un error.

Antes de acabar, mencionarte que hemos usado el operador tryMap para tener más granularidad y controlar qué ocurre en nuestro Publisher. Pero podríamos haber utilizado perfectamente el operador map:

let url = URL(string: "https://rickandmortyapi.com/api")!
let cancellable = URLSession.shared.dataTaskPublisher(for: url)
    /*.tryMap { (data, response) -> Data in
            guard let httpResponse = response as? HTTPURLResponse,
                httpResponse.statusCode == 200 else {
                    throw URLError(.badServerResponse)
                }
            return data
            }
     */
    .map(\.data)
    .decode(type: DataModel.self, decoder: JSONDecoder())
    .sink { completion in
        switch completion {
        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        case .finished:
            print("Finished")
        }
    } receiveValue: { value in
        print("Value: \(value)")
    }
Sustituimos el operador map por tryMap, el resultado es el mismo

Usando la API correcta, si volvemos a compilar, vemos que obtenemos el resultado correcto. El resultado es exactamente igual a lo que hemos visto en el ejemplo anterior.

Conclusión

Hoy hemos aprendido a cómo usar URLSession con Combine. Hemos usado un Publisher con varios operadores, tryMap, map y decode y luego nos hemos suscrito para recibir los valores de la petición HTTP y así transformar el JSON a un modelo de nuestro dominio.