Publishers 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)
}
}
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()
Si compilamos nuestro Playground, aparece por consola el siguiente resultado:
Values 35
Values 32
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)
}
}
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
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)
}
}
Al compilar obtenemos el siguiente mensaje por consola:
Values 35
Completion: failure(Error Domain=NSURLErrorDomain Code=-1000 "(null)")
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)")
}
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")
}
}
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()
Si ahora compilamos, el resultado que obtenemos es:
Values: Bienvenido SwiftBeta
Values: Crea una conversación con algún contacto
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")
}
}
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
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)")
}
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
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)")
}
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 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)")
}
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
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!