
Hoy me gustaría mostrarle cómo crear su propio editor en el nuevo marco Combine de Apple.
Entonces, para empezar, debemos recordar brevemente cómo interactúan las partes fundamentales de Combine entre sí, a saber, Editor, Suscripción, Suscriptor.
- El suscriptor se une al editor
- El editor envía el suscriptor de suscripción
- El suscriptor solicita N valores de la suscripción
- El editor envía N valores o menos
- El editor envía una señal de finalización
Editor
Entonces, comencemos a crear nuestro editor. En cuanto a la documentación de Apple, veremos que Publisher es un protocolo.
public protocol Publisher { associatedtype Output associatedtype Failure : Error func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input }
Donde Output es el tipo de valores pasados por este publicador, Failure es el tipo de error que debe seguir el protocolo Error.
Y la función de recepción (_: suscriptor), que se llamará para agregar un suscriptor a este publicador mediante la suscripción (_ :).
Como ejemplo, implementamos Publisher, que generará números de Fibonacci para nosotros.
struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never }
Dado que la secuencia consta de números, el tipo de salida será Int y Failure será el tipo especial Never, lo que indica que este publicador nunca fallará.
Para mayor flexibilidad, especificamos el límite en el número de elementos que queremos recibir y ajustamos este valor en algún objeto de configuración de nuestro publicador.
struct FibonacciConfiguration { var count: UInt }
Echemos un vistazo más de cerca a este código, var count: UInt parece una buena opción, pero su uso nos limita al rango de valores válidos de tipo UInt, y tampoco está claro qué indicar si todavía queremos tener una secuencia ilimitada.
En lugar de UInt, utilizaremos el tipo Subscribers.Demand, que se define en Combine, donde también se describe como el tipo que se envía desde el Suscriptor al Editor a través de la Suscripción. En términos simples, muestra la necesidad de elementos, cuántos elementos solicita el suscriptor. ilimitado - no limitado, ninguno - en absoluto, máximo (N) - no más de N veces.
public struct Demand : Equatable, Comparable, Hashable, Codable, CustomStringConvertible { public static let unlimited: Subscribers.Demand public static let none: Subscribers.Demand
Reescribimos la configuración de Fibonacci cambiando el tipo a uno nuevo para contar.
struct FibonacciConfiguration { var count: Subscribers.Demand }
Volvamos a Publisher e implementemos el método de recepción (_: Suscriptor), como recordamos, este método es necesario para agregar un Suscriptor a Publisher. Y lo hace con una Suscripción, el Editor debe crear una suscripción y transferir la suscripción al suscriptor.
func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) }
Esta es una función genérica que toma al Suscriptor como parámetro, y los valores de salida del Editor deben coincidir con los valores de entrada del Suscriptor (Salida == S.Input), lo mismo para los errores. Esto es necesario para "conectar" Publisher'a y Subscriber'a.
En la función misma, cree una suscripción de suscripción de Fibonacci, en el constructor transferimos el suscriptor y la configuración. Después de eso, la suscripción se transfiere al suscriptor.
Nuestro editor está listo, al final tenemos:
struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never var configuration: FibonacciConfiguration func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } }
Como puede ver, Publisher en sí no contiene ninguna lógica para generar una secuencia de Fibonacci; toda la lógica estará en la clase de suscripción: FibonacciSubscription.
Como ya puede adivinar, la clase FibonacciSubscription seguirá el protocolo de suscripción, veamos la definición de este protocolo.
public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible { func request(_ demand: Subscribers.Demand) }
La función request (_: Subscribers.Demand) le dice a Publisher que puede enviar más valores al suscriptor. Es en este método que la lógica de enviar números de Fibonacci será.
También necesitamos implementar siguiendo el protocolo Cancelable e implementar la función cancel ().
public protocol Cancellable { func cancel() }
Y solo siga el protocolo CustomCombineIdentifierConvertible y defina la variable de solo lectura combineIdentifier.
public protocol CustomCombineIdentifierConvertible { var combineIdentifier: CombineIdentifier { get } }
Aquí hay una aclaración, si se desplaza justo debajo de la definición del protocolo CustomCombineIdentifierConvertible en Combine, puede ver que Combine proporciona una extensión para este protocolo, que tiene la forma:
extension CustomCombineIdentifierConvertible where Self : AnyObject { public var combineIdentifier: CombineIdentifier { get } }
Lo que nos dice que la definición de la variable combineIdentifier: CombineIdentifier se proporciona por defecto si el tipo que sigue este protocolo también sigue el protocolo AnyObject, es decir, si este tipo es una clase. FibonacciSubscription es una clase, por lo que obtenemos la definición de variable predeterminada.
Suscripción
Y así comenzaremos a implementar nuestra suscripción de Fibonacci.
private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } ... }
Como puede ver, FibonacciConfiguration contiene un fuerte vínculo con el suscriptor, en otras palabras, es el propietario del suscriptor. Este es un punto importante, la suscripción es responsable de la retención del suscriptor, y debe ser retenido por él hasta que finalice el trabajo, finalice con un error o cancelado.
A continuación, implementamos el método cancel () del protocolo Cancelable.
func cancel() { subscriber = nil }
Establecer el suscriptor en nil lo hace inaccesible para una suscripción.
Ahora estamos listos para comenzar la implementación del envío de números de Fibonacci.
implementamos el método de solicitud (_: Subscribers.Demand).
func request(_ demand: Subscribers.Demand) {
1) Desde el principio, verificamos cuántos elementos nos puede proporcionar Publisher, si no es que lo hacemos, luego completamos el envío y enviamos al Suscriptor una señal de que se ha completado el envío de números.
2) Si es necesario, reduzca el número total de números solicitados en uno, envíe el primer elemento de la secuencia de Fibonacci al Suscriptor, es decir, 0 y luego verifique nuevamente cuántos elementos más puede proporcionarnos el Editor, si no, envíe una señal al Suscriptor para completar .
3) El mismo enfoque que en el párrafo 2), pero solo para el segundo elemento en la secuencia de Fibonacci.
4) Si se requieren más de 2 elementos, implementamos un algoritmo iterativo para encontrar números de Fibonacci, donde en cada paso transferiremos el siguiente número de la secuencia de Fibonacci a Subscriber'y y también verificaremos cuántos elementos todavía puede proporcionar Publisher. Si el editor ya no proporciona nuevos números, envíe al suscriptor una señal de finalización.
Por el momento, hemos escrito dicho código struct FibonacciConfiguration { var count: Subscribers.Demand } struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never var configuration: FibonacciConfiguration func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } } private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } func cancel() { subscriber = nil } func request(_ demand: Subscribers.Demand) {
Primera prueba
Ahora probamos lo que tenemos, tenemos nuestro editor y suscripción, nos falta Sibscriber, Combine proporciona 2 Sibscriber listos para usar, esto es hundir y asignar.
- sumidero: este método crea un suscriptor e inmediatamente solicita un número ilimitado de valores.
- asignar: establece cada elemento de Publisher en una propiedad de objeto.
El sumidero es adecuado para nuestro propósito, se debe prestar especial atención al hecho de que solicita un número ilimitado de valores.
Y aquí debemos hacer una distinción importante, nuestro Editor en la variable de conteo determina el número de elementos que nuestro Editor puede proporcionar y estas condiciones las determinamos nosotros mismos. En principio, podríamos prescindir de esta variable y no estar limitados en la transmisión de números de Fibonacci, pero muy pronto iríamos más allá del rango de valores admisibles del tipo Int.
El caso con sumidero es diferente, cada suscriptor determina cuántos valores desea recibir, sumidero solicita un número ilimitado de valores, lo que significa que recibirá valores hasta que reciba una señal de finalización, error o cancelación.
Para la conveniencia de utilizar nuestro editor, agregamos su creación a la extensión de protocolo del editor.
extension Publishers { private static func fibonacci(configuration: FibonacciConfiguration) -> FibonacciPublisher { FibonacciPublisher(configuration: configuration) } static func fibonacci(count: Subscribers.Demand = .max(6)) -> FibonacciPublisher { FibonacciPublisher(configuration: FibonacciConfiguration(count: count)) } }
Y prueba nuestro editor
Publishers.fibonacci(count: .max(10)) .sink { value in print(value, terminator: " ") }
Y ahora los casos límite
Publishers.fibonacci(count: .max(1)) .sink { value in print(value, terminator: " ") }
Pero, ¿qué sucede si especifica .unlimited?
Publishers.fibonacci(count: .unlimited) .print() .sink { value in print(value, terminator: " ") }
¿Cómo puedes usar .unlimited, pero ser capaz de generar múltiples números? Para hacer esto, necesitamos el operador .prefix (_), que funciona de la misma manera que .prefix (_) de las colecciones, es decir, deja solo los primeros N elementos.
Publishers.fibonacci(count: .unlimited) .print() .prefix(5) .sink { _ in }
Cual es el problema Tal vez en .prefix (_)? Hagamos un pequeño experimento sobre la secuencia estándar de Foundation.
Como podemos ver, el código anterior funcionó correctamente, entonces el problema está en nuestra implementación de Publisher.
Observamos los registros de .print () y vemos que después de N solicitudes, desde .prefix (_), llamamos a cancel () en nuestra FibonacciSubscription, donde configuramos el suscriptor como nulo.
func cancel() { subscriber = nil }
Si abre la pila de llamadas, puede ver que se llama a cancel () desde la solicitud (_ :), es decir, durante la llamada al suscriptor? .Receive (_). De lo cual podemos concluir que en algún momento dentro de la solicitud (_ :) el suscriptor puede ser nulo y luego debemos detener el trabajo de generar nuevos números. Agregue esta condición a nuestro código.
func request(_ demand: Subscribers.Demand) {
Ahora ejecuta nuestro código de prueba.
Publishers.fibonacci(count: .unlimited) .print() .prefix(5) .sink { _ in }
Obtuve el comportamiento esperado.
Suscriptor
¿Y así está lista nuestra suscripción de Fibonacci? En realidad, no, en nuestras pruebas solo usamos un suscriptor de sumidero que solicita un número ilimitado de números, pero ¿qué sucede si usamos un suscriptor que esperará un cierto número limitado de números? Combine no proporciona ese suscriptor, pero ¿qué nos impide escribir el nuestro? A continuación se muestra la implementación de nuestro suscriptor de Fibonacci.
class FibonacciSubscriber: Subscriber { typealias Input = Int typealias Failure = Never var limit: Subscribers.Demand init(limit: Subscribers.Demand) { self.limit = limit } func receive(subscription: Subscription) { subscription.request(limit) } func receive(_ input: Input) -> Subscribers.Demand { .none } func receive(completion: Subscribers.Completion<Failure>) { print("Subscriber's completion: \(completion)") } }
Y así, nuestro Suscriptor Fibonacci tiene una propiedad de límite, que determina cuántos elementos desea recibir este Suscriptor. Y esto se hace en el método de recepción (_: Suscripción), donde le decimos a la suscripción cuántos elementos necesitamos. También vale la pena señalar la función recibir (_: Entrada) -> Suscriptores. Demanda, esta función se llama cuando se recibe un nuevo valor, como el valor de retorno indicamos cuántos elementos adicionales queremos recibir: .ninguno, en absoluto, .max (N) N piezas , en total, el número total de elementos recibidos será igual a la suma del valor de la suscripción enviada en recepción (_: Suscripción) y todos los valores devueltos de recepción (_: Entrada) -> Suscriptores. Demanda.
Segunda prueba
Intentemos usar FibonacciSubscriber.
let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber)
Como vemos, nuestro editor envió 5 valores, en lugar de 3. ¿Por qué? Debido a que el método de solicitud (_: Subscribers.Demand) de FibonacciSubscription'a no tiene en cuenta las necesidades del suscriptor, solucionémoslo, para esto agregaremos una propiedad adicional solicitada, a través de la cual rastrearemos las necesidades del suscriptor.
private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand var requested: Subscribers.Demand = .none
Tercera prueba
let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber)
El editor ahora funciona correctamente.
Código final import Foundation import Combine struct FibonacciConfiguration { var count: Subscribers.Demand } struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never var configuration: FibonacciConfiguration func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { let subscription = FibonacciSubscription(subscriber: subscriber, configuration: configuration) subscriber.receive(subscription: subscription) } } private final class FibonacciSubscription<S: Subscriber>: Subscription where S.Input == Int { var subscriber: S? var configuration: FibonacciConfiguration var count: Subscribers.Demand var requested: Subscribers.Demand = .none init(subscriber: S?, configuration: FibonacciConfiguration) { self.subscriber = subscriber self.configuration = configuration self.count = configuration.count } func cancel() { subscriber = nil } func request(_ demand: Subscribers.Demand) { guard count > .none else { subscriber?.receive(completion: .finished) return } requested += demand count -= .max(1) requested -= .max(1) requested += subscriber?.receive(0) ?? .none guard let _ = subscriber, requested > .none else { return } if count == .none { subscriber?.receive(completion: .finished) return } count -= .max(1) requested -= .max(1) requested += subscriber?.receive(1) ?? .none guard let _ = subscriber, requested > .none else { return } if count == .none { subscriber?.receive(completion: .finished) return } var prev = 0 var current = 1 var temp: Int while let subscriber = subscriber, requested > .none { temp = prev prev = current current += temp requested += subscriber.receive(current) count -= .max(1) requested -= .max(1) if count == .none { subscriber.receive(completion: .finished) return } } } } extension Publishers { private static func fibonacci(configuration: FibonacciConfiguration) -> FibonacciPublisher { FibonacciPublisher(configuration: configuration) } static func fibonacci(count: Subscribers.Demand = .max(6)) -> FibonacciPublisher { FibonacciPublisher(configuration: FibonacciConfiguration(count: count)) } } class FibonacciSubscriber: Subscriber { typealias Input = Int typealias Failure = Never var limit: Subscribers.Demand init(limit: Subscribers.Demand) { self.limit = limit } func receive(subscription: Subscription) { subscription.request(limit) } func receive(_ input: Input) -> Subscribers.Demand { .none } func receive(completion: Subscribers.Completion<Failure>) { print("Subscriber's completion: \(completion)") } } Publishers.fibonacci(count: .max(4)) .print() .sink { _ in } let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber)
Resultado
Espero que este artículo le haya ayudado a comprender mejor qué son Publisher, Subscription y Subscriber, cómo interactúan entre sí y a qué puntos debe prestar atención cuando decida implementar su Publisher. Cualquier comentario, aclaraciones al artículo son bienvenidas.