DEV Community

David Goyes
David Goyes

Posted on

Combine #9: Conexión a internet

Extensiones de URLSession

URLSession puede hacer lo siguiente:

  • Transferir datos a una URL.
  • Descargar el contenido de una URL como un archivo binario que se guarda en el sistema de ficheros.
  • Subir un archivo binario a una URL.
  • Tareas de Streams para comunicar dos partes por medio de flujos de datos (streams).
  • Tareas de Websockets para comunicar dos sockets.

Entre todas estas funcionalidades, URLSession solo expone la primera (transferir datos a una URL) con una interfaz de Combine por medio del operador .dataTaskPublisher(for:). En el siguiente ejemplo se puede ver cómo descargar los datos de un sitio web. Aquí no hay que olvidar almacenar la suscripción, porque la tarea nunca iniciará si la suscripción se elimina. Observar que los datos descargados llegan por el closure de receiveValue, pero igual hay que procesar el evento de fin (receiveCompletion) donde puede venir un posible error.

let url = URL(string: "https://goyesdev.com/mydata.json")!
URLSession.shared
  .dataTaskPublisher(for: url)
  .sink(receiveCompletion: { completion in
    if case .failure(let err) = completion {
      print("Hubo un error")
    }
  }, receiveValue: { data, response in
    print("Descargué unos datos de tamaño \(data.count), y la respuesta fue: \(response)")
  })
  .store(in: &subscriptions)
Enter fullscreen mode Exit fullscreen mode

Soporte a Codable

Se puede usar JSONDecoder para decodificar una respuesta web. A continuación presento el código para hacerlo de forma imperativa con tryMap. Cabe resaltar que cada vez que se recibe una nueva respuesta, se vuelve a crear el JSONDecoder dentro del tryMap.

let subscription = URLSession.shared
  .dataTaskPublisher(for: url)
  .tryMap { data, _ in
    try JSONDecoder().decode(MyType.self, from: data)
  }
  .sink( ... )
Enter fullscreen mode Exit fullscreen mode

El operador decode(type:decoder:) permite hacerlo de forma declarativa:

let subscription = URLSession.shared
  .dataTaskPublisher(for: url)
  .map(\.data)
  .decode(type: MyType.self, decoder: JSONDecoder())
  .sink( ... )
Enter fullscreen mode Exit fullscreen mode

Como .dataTaskPublisher(for:) emite tuplas de tipo (Data, URLResponse), es necesario aplicar map antes de decode(type:decoder:) para extraer el primer dato.

Publicando datos de una petición web a múltiples suscriptores

Es posible que varios clientes necesiten suscribirse al resultado de la misma petición web, de tipo Publisher. Por defecto, crear dos suscripciones al mismo Publisher provocará dos ejecuciones del mismo pipeline, o sea, en este caso: dos peticiones web. El operador share() puede ayudar a crear un observador caliente para no iniciar dos peticiones web, sin embargo, la petición empieza tan pronto ocurre la primera suscripción y es posible que alguna suscripción posterior no alcance a recibir el resultado.

Ante esta problemática, hay que crear manualmente un multicast (que crea un ConnectablePublisher que emite valores a través de un Subject) y hacerle connect cuando ya estén listas todas las suscripciones.

En este caso, tener presente guardar todas las suscripciones.

let url = URL(string: "https://www.raywenderlich.com")!
let publisher = URLSession.shared
  .dataTaskPublisher(for: url)
  .map(\.data)
  // Se crea el multicast
  .multicast { PassthroughSubject<Data, URLError>() }
// Agregar suscriptores al multicast
let subscription1 = publisher.sink( ... )
let subscription2 = publisher.sink( ... )
// Conectar el ConnectablePublisher
let subscription = publisher.connect()
Enter fullscreen mode Exit fullscreen mode

Cuestionario

1. ¿Qué tipo de tarea de URLSession tiene soporte directo para Combine mediante dataTaskPublisher(for:)?

2. ¿Por qué es necesario almacenar la suscripción de un dataTaskPublisher en una variable o colección?

3. ¿Qué diferencia práctica hay entre usar tryMap y decode(type:decoder:) para decodificar datos JSON?

4. ¿Por qué se aplica map(\.data) antes de usar decode(type:decoder:) en un pipeline con dataTaskPublisher?

5. Explica por qué usar solo share() puede causar que algunas suscripciones no reciban datos en una petición web compartida.

6. ¿Qué emite el publisher creado con URLSession.shared.dataTaskPublisher(for:)? ✅

  • [ ] Solo los datos (Data)
  • [ ] Una tupla (Data, URLResponse)
  • [ ] Solo la respuesta (URLResponse)

7. ¿Qué hace el operador decode(type:decoder:) en Combine? ✅

  • [ ] Convierte los datos binarios en texto plano
  • [ ] Usa un JSONDecoder u otro decodificador para transformar los datos en un tipo Codable
  • [ ] Filtra las respuestas HTTP exitosas

8. ¿Qué ocurre si creas dos suscripciones independientes al mismo dataTaskPublisher sin usar share() ni multicast()? ✅

  • [ ] Se realiza solo una petición y se comparte la respuesta
  • [ ] Se realizan dos peticiones web separadas 
  • [ ] La segunda suscripción queda en espera

9. ¿Qué operador permite crear manualmente un ConnectablePublisher que se inicia con .connect()? ✅

  • [ ] multicast
  • [ ] share
  • [ ] makeConnectable

10. ¿Cuál es la función principal del método .connect() en un publisher multicasted? ✅

  • [ ] Cancelar las suscripciones activas
  • [ ] Iniciar la emisión de valores hacia todos los suscriptores 
  • [ ] Reiniciar el pipeline desde cero

Solución

1. ¿Qué tipo de tarea de URLSession tiene soporte directo para Combine mediante dataTaskPublisher(for:)?

La tarea de transferir datos a un servidor (dataTask).

2. ¿Por qué es necesario almacenar la suscripción de un dataTaskPublisher en una variable o colección?

En caso contrario, el Publisher se cancela cuando el scope donde se creó la suscripción termina.

3. ¿Qué diferencia práctica hay entre usar tryMap y decode(type:decoder:) para decodificar datos JSON?

tryMap requiere escribir la lógica de decodificación dentro del closure, mientras que decode(type:decoder:) es el operador especializado de Combine que hace el trabajo automaticamente de forma declarativa. Además, solo se necesita crear una vez el decodificador.

4. ¿Por qué se aplica map(\.data) antes de usar decode(type:decoder:) en un pipeline con dataTaskPublisher?

Porque dataTaskPublisher(for:) devuelve una tupla y solo necesitamos pasar los datos al siguiente paso del pipeline (i.e. decode(type:decoder:)).

5. Explica por qué usar solo share() puede causar que algunas suscripciones no reciban datos en una petición web compartida.

share() inicia la petición cuando se suscribe el primer observador; si la petición ya terminó, los nuevos suscriptores no recibirán el resultado.

6. ¿Qué emite el publisher creado con URLSession.shared.dataTaskPublisher(for:)

  • [✅] Una tupla (Data, URLResponse)

7. ¿Qué hace el operador decode(type:decoder:) en Combine? 

  • [✅] Usa un JSONDecoder u otro decodificador para transformar los datos en un tipo Codable

8. ¿Qué ocurre si creas dos suscripciones independientes al mismo dataTaskPublisher sin usar share() ni multicast()

  • [✅] Se realizan dos peticiones web separadas

9. ¿Qué operador permite crear manualmente un ConnectablePublisher que se inicia con .connect()

  • [✅] multicast

10. ¿Cuál es la función principal del método .connect() en un publisher multicasted? 

  • [✅] Iniciar la emisión de valores hacia todos los suscriptores

Top comments (0)