Aprende Publishers en Combine: PassthroughtSubject y CurrentValueSubject
Aprende Publishers en Combine: PassthroughtSubject y CurrentValueSubject

Publishers Combine: PassthroughSubject y CurrentValueSubject

PassthroughSubject y CurrentValueSubject son dos publishers que nos permiten publicar eventos en un Publisher de forma imperativa. Usamos un método llamado send para enviar los eventos. La diferencia entre PassthroughSubject y CurrentValueSubject es que al último le damos un valor por defecto

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
🤩 ¡Sígueme en Twitter!
▶️ ¡Suscríbete al canal!
🎁 ¡Apoya el canal!

Publishers en Combine: PassthroughSubject y CurrentValueSubject

Hoy en SwiftBeta vamos a aprende sobre dos Publishers que podemos usar con el framework Combine. Son PassthroughSubject y CurrentValueSubject, al usarlos podemos enviar valores a los subscribers de forma imperativa, es decir, podemos usar un método llamado send para publicar los valores en nuestro Publisher. Y la única diferencia entre estos dos Publishers, es que con CurrentValueSubject podemos darle un valor por defecto, es decir, cuando lo inicializamos le damos un valor que más tarde cuando un subscriber se suscriba lo recibirá automáticamente. En cambio, PassthroughSubject lo inicializamos sin ningún valor, y por lo tanto después de suscribirnos deberemos enviar algún valor para que el subscriber reciba valores. Y en ambos Publishers usamos el método send para enviar valores en nuestra pipeline.

Hoy vamos a ver ejemplos muy prácticos y vamos a crear una nueva page en nuestro Playground. Vamos a simular que tenemos una app del pronóstico del tiempo que nos va publicando temperaturas en un Publisher y para hacerlo vamos a crear un PassthroughSubject.

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

PasstroughSubject

Para hacerlo, voy a crear una struct llamada Weather, voy a poner el modo rápido y luego entraré en detalle en cada parte del código:

import Combine

struct Weather {
    let weatherPublisher = PassthroughSubject<Int, Error>()
    
    func getWeatherInfo() {
        weatherPublisher.send(35)
        weatherPublisher.send(32)
    }
}
Struct con un Publisher y un método para enviar valores a través del Publisher

Para crear nuestro PassthroughSubject, fíjaque que hemos creado una propiedad. En esta propiedad hemos especificado el Output y el Failure. El Output es el tipo Int, que representará las temperaturas que enviaremos en nuestro Publisher. Y el Failure es el tipo Error, ya que en este caso podremos enviar errores a través de nuestro Publisher.

A parte de la propiedad, hemos creado un método llamado getWeatherInfo que dentro usa el método send, este método lo usamos para enviar los valores de tipo Int que se enviarán para que el subscriber pueda recuperar esos valores, esas temperaturas.

Ahora, vamos a usar el método sink para suscribirnos y poder recoger los valores que envía el publisher:

let weather = Weather()
weather.weatherPublisher.sink { completion in
    print("Completion: \(completion)")
} receiveValue: { values in
    print("Values \(values)")
}

weather.getWeatherInfo()
Suscripción al publisher usand el método sink

Si compilamos nuestro Playground, aparece por consola el siguiente resultado:

Values 35
Values 32
Valores recibidos en el Subscriber

Pero, como te decía hace un rato, no solo podemos enviar enteros, también como vimos en el anterior video, una vez hemos enviado todos los valores en nuestra pipeline, podemos enviar un valor a través de la pipeline que indique al subscriber que hemos finalizado.
Y es curioso, que después de enviar que nuestro Publisher ha finalizado, cualquier otro valor que enviemos, nunca se enviará en nuestro Publisher y por lo tanto el Subscriber no lo recibirá. Vamos a verlo en un ejemplo:

struct Weather {
    let weatherPublisher = PassthroughSubject<Int, Error>()
    
    func getWeatherInfo() {
        weatherPublisher.send(35)
        weatherPublisher.send(completion: .finished)
        weatherPublisher.send(32)
    }
}
Enviamos un completion tipo .finished en nuestro Publisher

Para indicar que el Publisher ha finalizado de enviar valores, hemos usado un enum que su case es .finished. Si compilamos, vamos a ver qué ocurre:

Values 35
Completion: finished
Al enviar un finished en nuestro Publisher no recibimos más valores

Pero, imagina que en lugar de indicar que hemos finalizado de publicar todos los valores, imagina que hemos obtenido un error, por el motivo que sea, en este caso, en lugar de enviar un .finished, podríamos enviar un error, ya verás que sencillo es, acontinuación vamos a enviar un error ya definido en la librería standard de swift:

struct Weather {
    let weatherPublisher = PassthroughSubject<Int, Error>()
    
    func getWeatherInfo() {
        weatherPublisher.send(35)
        weatherPublisher.send(completion: .failure(URLError(.badURL)))
        weatherPublisher.send(32)
    }
}
Si enviamos un error a través de nuestro Publisher, no recibimos más valores en nuestro Subscriber

Al compilar obtenemos el siguiente mensaje por consola:

Values 35
Completion: failure(Error Domain=NSURLErrorDomain Code=-1000 "(null)")
Resultado obtenido en nuestro Subscriber

Poco a poco ya vamos entendiendo cada vez más como funciona el framework Combine. Pero quizás te preguntas ¿Podríamos diferenciar en el método sink cuando el publisher finaliza correctamente o obtenemos un error? Y la respuesta es que sí, tan solo necesitamos usar un switch:

let weather = Weather()
weather.weatherPublisher.sink { completion in
    switch completion {
    case .failure(let error):
        print("Error \(error.localizedDescription)")
    case .finished:
        print("Finished")
    }
    //print("Completion: \(completion)")
} receiveValue: { values in
    print("Values \(values)")
}
Podemos diferenciar cuando el completion es por finished o por failure dentro del método sink

Al hacerlo de esta manera podemos separar el flujo de nuestra app y realizar una lógica si hemos completado correctamente o por lo contrario hemos obtenido un error.

Al usar PassthroughtSubject podemos enviar eventos según nuestra necesidad, es decir, podemos enviar un valor o especificar un completado o error dentro de nuestro Publisher. Nos da mucha flexibilidad. Ahora vamos a ver qué diferencia hay con CurrentValueSubject y vamos a introducir más conceptos interesantes de combine.

CurrentValueSubject

Ahora, en lugar de crear un ejemplo sobre el pronóstico del tiempo, vamos a crear un ejemplo que simule que hemos creado una app de chat. Imagina, te bajas telegram o whatsapp, y nada más hacerlo quieres que se te cree una conversación vacía de un bot explicándote el onboarding de la app. El onboarding es un tutorial con mensajes que vas recibiendo en una conversación, te va orientando y te va dando consejos de cómo usar la app, por ejemplo nada más instalar la app recibes un mensaje: Primer mensaje, crea una conversación con algún contacto. Al cabo de X segundos de usarla recibes otro mensaje, este segundo mensaje podría ser: Envía un sticker, etc Vamos a simular este comportamiento en una struct llamada BotApp:

struct BotApp {
    var onboardingPublisher = CurrentValueSubject<String, Error>("Bienvenido SwiftBeta")
    
    func startOnboarding() {
        onboardingPublisher.send("Crea una conversación con algún contacto")
    }
}
Struct BotApp con un publisher y un método para enviar Strings a nuestro Subscriber

Vamos hacer exactamente lo mismo que hemos visto en el ejemplo anterior con PassthroughSubject, pero esta vez la propiedad será de tipo CurrentValueSubject. En este caso, nuestra propiedad CurrentValueSubject ya tiene un valor por defecto de "Bienvenido a SwiftBeta", que se recibirá una vez nos suscribamos a este publisher. Y en este caso, el Output de nuestro Publisher es de tipo String, y el error es de tipo Error. Ahora, vamos a suscribirnos a nuestro Publisher:

let botApp = BotApp()
let cancellation = botApp.onboardingPublisher.sink { completion in
    switch completion {
    case .failure(let error):
        print("Error \(error.localizedDescription)")
    case .finished:
        print("Finished")
    }
} receiveValue: { values in
    print("Values: \(values)")
}

botApp.startOnboarding()
Subscriber conectada al Publisher para recibir todos los valores que se envien

Si ahora compilamos, el resultado que obtenemos es:

Values: Bienvenido SwiftBeta
Values: Crea una conversación con algún contacto
Resultado obtenido por consola

Perfecto, ahora vamos a simular que pasan 2 segundos y 6 segundos para enviar más valores en nuestro publisher llamado onboardingPublisher. Para hacerlo podemos usar DispatchQueue:

func startOnboarding() {
    onboardingPublisher.send("Crea una conversación con algún contacto")
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        onboardingPublisher.send("Envía un Sticker")
    }
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
        onboardingPublisher.send("Activa las notificaciones")
    }
}
Simulamos que recibimos varios valores en nuestro Subscriber cuando pasan X segundos

Con el anterior código vamos a publicar 3 valores a nuestro onboardingPublisher. Vamos a compilar para ver qué ocurre. SPOILER: No vamos a recibir los valores que tenemos dentro de los DispatchQueue

Al compilar seguimos recibiendo estos valores por consola:

Values: Bienvenido SwiftBeta
Values: Crea una conversación con algún contacto
No recibimos los nuevos valores, tenemos un error

Algo está ocurriendo, podemos debuggar nuestro publisher usando un método llamado handleEvents. Con este método podemos saber qué ocurre exactamente en nuestro publisher. Y para usarlo, ponemos el siguiente código:

botApp.onboardingPublisher.handleEvents { subscription in
    print("1. Subscription Received: \(subscription)")
} receiveOutput: { value in
    print("2. Value Received: \(value)")
} receiveCompletion: { completion in
    print("3. Completion Received: \(completion)")
} receiveCancel: {
    print("4. Cancel Received")
} receiveRequest: { request in
    print("5. Request Received")
}.sink { completion in
    switch completion {
    case .failure(let error):
        print("Error \(error.localizedDescription)")
    case .finished:
        break
        //print("Finished")
    }
} receiveValue: { values in
    //print("Values: \(values)")
}
Debuggamos nuestro Publisher con el método handleEvents
Todos los closures del método handleEvents son opcionales, es por eso que podemos omitir y borrar el que queramos.

He comentado algunas líneas del método sink para que sea más fácil de debuggar. Si compilamos, obtenemos el siguiente resultado por consola:

1. Subscription Received: CurrentValueSubject
5. Request Received
2. Value Received: Bienvenido SwiftBeta
2. Value Received: Crea una conversación con algún contacto
4. Cancel Received
Resultado obtenido de lo que ocurre en nuestro Publisher

La información que obtenemos es muy valiosa, y con el último mensaje podemos hacernos una idea de que está ocurriendo, el que aparece Cancel Received, algo está ocurriendo que nuestro subscriber ha dejado de escuchar lo que envía el Publisher. En el anterior video, te comentaba que lo publishers tienen un ciclo de vida, en nuestro ejemplo, no hay nada que esté reteniendo a nuestro publisher en memoria, y por lo tanto se acaba liberando.

Para evitar que se libere podemos guardar una referencia en el tipo AnyCancellable. Y es tan sencillo como hacer lo siguiente:

let cancellable = botApp.onboardingPublisher.handleEvents { subscription in
    print("1. Subscription Received: \(subscription)")
} receiveOutput: { value in
    print("2. Value Received: \(value)")
} receiveCompletion: { completion in
    print("3. Completion Received: \(completion)")
} receiveCancel: {
    print("4. Cancel Received")
} receiveRequest: { request in
    print("5. Request Received")
}.sink { completion in
    switch completion {
    case .failure(let error):
        print("Error \(error.localizedDescription)")
    case .finished:
        break
        //print("Finished")
    }
} receiveValue: { values in
    //print("Values: \(values)")
}
Asignamos el valor de .sink a una constante

Al usar la constante cancellable, si ejecutamos nuestro código en el playground, observamos que vamos obteniendo los valores que se publican en nuestroi publisher:

1. Subscription Received: CurrentValueSubject
5. Request Received
2. Value Received: Bienvenido SwiftBeta
2. Value Received: Crea una conversación con algún contacto
2. Value Received: Envía un Sticker
2. Value Received: Activa las notificaciones
Ahora recibimos todos los valores que enviamos en nuestro Publisher

Ahora que ya entendemos lo que ocurre, podemos dejar de usar el método handleEvents y quedarnos solo con el método sink:

let cancellable = botApp.onboardingPublisher.sink { completion in
    switch completion {
    case .failure(let error):
        print("Error \(error.localizedDescription)")
    case .finished:
        print("Finished")
    }
} receiveValue: { values in
    print("Values: \(values)")
}
Refactorizamos el código y usamos solo el método sink

vamos a compilar y vamos a ver el resultado que aparece por consola:

Values: Bienvenido SwiftBeta
Values: Crea una conversación con algún contacto
Values: Envía un Sticker
Values: Activa las notificaciones
Resultado que mostramos por consola

Conclusión

Hoy hemos aprendido a usar dos publishers en los que podemos enviar valores cuando queramos. Estos publishers tienen un método llamado send en el que podemos enviar valores, errores o indicar que el publisher ha completado de enviar eventos.

También hemos aprendido la diferencia entre PassthroughtSubjet y CurrentValueSubject. Su difierencia es que con CurrentValueSubject podemos asignar un valor por defecto al Publisher para que cuando un subscriber se suscriba, reciba ese valor sin tener que ser enviado explicitamente por el publisher.

Y hasta aquí el video de hoy!