Crie seu editor em Combine


Hoje, gostaria de mostrar como criar seu próprio editor na nova estrutura Combine da Apple.


E, portanto, para iniciantes, precisamos lembrar brevemente como as partes fundamentais do Combine interagem entre si, ou seja, Publicador, Assinatura, Assinante.


  • O assinante ingressa no Publisher
  • O editor envia o Assinante da Assinatura
  • O assinante solicita N valores da assinatura
  • O editor envia N valores ou menos
  • O editor envia um sinal de conclusão

Publisher


Então, vamos começar a criar nosso Publicador. Voltando à documentação da Apple, veremos que o Publisher é um 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 } 

Onde Saída é o tipo de valores transmitidos por este Publicador, Falha é o tipo de erro que deve seguir o protocolo de Erro.


E a função de recebimento (_: Assinante), que será chamada para adicionar Assinante a este Publicador usando a assinatura (_ :).


Como exemplo, implementamos o Publisher, que irá gerar números de Fibonacci para nós.


 struct FibonacciPublisher: Publisher { typealias Output = Int typealias Failure = Never } 

Como a sequência consiste em números, o tipo de saída será Saída do tipo Int e Failure será um tipo especial de Never, indicando que este Publicador nunca falhará.


Para flexibilidade, especificamos o limite do número de elementos que queremos receber e agrupamos esse valor em algum objeto de configuração do nosso Publicador.


 struct FibonacciConfiguration { var count: UInt } 

Vamos dar uma olhada mais de perto neste código, var count: UInt parece uma boa opção, mas seu uso nos limita ao intervalo de valores válidos do tipo UInt, e também não está claro o que indicar se ainda queremos ter uma sequência ilimitada.


Em vez do UInt, usaremos o tipo Subscribers.Demand, definido em Combine, onde também é descrito como o tipo enviado do Assinante para o Publisher através da Assinatura. Em termos simples, mostra a necessidade de elementos, quantos elementos são solicitados pelo Assinante. ilimitado - não limitado, nenhum - nem um pouco, máximo (N) - não mais que N vezes.


  public struct Demand : Equatable, Comparable, Hashable, Codable, CustomStringConvertible { public static let unlimited: Subscribers.Demand public static let none: Subscribers.Demand ///  Demand.max(0) @inlinable public static func max(_ value: Int) -> Subscribers.Demand .... } 

Reescrevemos FibonacciConfiguration alterando o tipo para um novo para contagem.


 struct FibonacciConfiguration { var count: Subscribers.Demand } 

Vamos voltar ao Publisher e implementar o método receive (_: Subscriber), como lembramos, esse método é necessário para adicionar o Subscriber ao Publisher. E ele faz isso com uma assinatura, o Publisher deve criar uma assinatura e transferir a assinatura para o assinante.


  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) } 

Essa é uma função genérica que usa o Assinante como parâmetro e os valores de saída do Publicador devem corresponder aos valores de entrada do Assinante (Saída == S.Input), o mesmo para erros. Isso é necessário para "conectar" Publisher'a e Subscriber'a.


Na própria função, crie uma assinatura FibonacciSubscription, no construtor transferimos o assinante e a configuração. Depois disso, a assinatura é transferida para o assinante.


Nosso editor está pronto, e no final temos:


 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 você pode ver, o próprio Publisher não contém nenhuma lógica para gerar uma sequência de Fibonacci; toda a lógica estará na classe de assinatura - FibonacciSubscription.


Como você já deve adivinhar, a classe FibonacciSubscription seguirá o protocolo Subscription, vejamos a definição desse protocolo.


 public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible { func request(_ demand: Subscribers.Demand) } 

A função de solicitação (_: Subscribers.Demand) informa ao Publisher que ele pode enviar mais valores ao assinante. É neste método que será a lógica do envio de números de Fibonacci.
Também precisamos implementar seguindo o protocolo Cancellable e implementar a função cancel ().


 public protocol Cancellable { func cancel() } 

E basta seguir o protocolo CustomCombineIdentifierConvertible e definir a variável somente leitura combineIdentifier.


 public protocol CustomCombineIdentifierConvertible { var combineIdentifier: CombineIdentifier { get } } 

Há um esclarecimento aqui, se você rolar logo abaixo da definição do protocolo CustomCombineIdentifierConvertible no Combine, poderá ver que o Combine fornece uma extensão para esse protocolo, que tem o formato -


 extension CustomCombineIdentifierConvertible where Self : AnyObject { public var combineIdentifier: CombineIdentifier { get } } 

O que nos diz que a definição da variável combineIdentifier: CombineIdentifier é fornecida por padrão se o tipo que segue este protocolo também seguir o protocolo AnyObject, ou seja, se esse tipo for uma classe. FibonacciSubscription é uma classe, então obtemos a definição de variável padrão.


Assinatura


E assim começaremos a implementar nossa FibonacciSubscription.


 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 você pode ver, o FibonacciConfiguration contém um forte vínculo com o Assinante, em outras palavras, é o proprietário do Assinante. Este é um ponto importante, a assinatura é responsável pela retenção do assinante e deve mantê-la até que termine o trabalho, termine com um erro ou cancele.


Em seguida, implementamos o método cancel () do protocolo Cancellable.


 func cancel() { subscriber = nil } 

Definir assinante como nulo torna inacessível a assinatura.


Agora estamos prontos para começar a implementação do envio de números de Fibonacci.
implementamos o método de solicitação (_: Subscribers.Demand).


 func request(_ demand: Subscribers.Demand) { // 1 guard count > .none else { subscriber?.receive(completion: .finished) return } // 2 count -= .max(1) subscriber?.receive(0) if count == .none { subscriber?.receive(completion: .finished) return } // 3 count -= .max(1) subscriber?.receive(1) if count == .none { subscriber?.receive(completion: .finished) return } // 4 var prev = 0 var current = 1 var temp: Int while true { temp = prev prev = current current += temp subscriber?.receive(current) count -= .max(1) if count == .none { subscriber?.receive(completion: .finished) return } } } 

1) Desde o início, verificamos quantos elementos o Publisher pode nos fornecer, se não houver, em seguida, concluímos o envio e enviamos um sinal ao Assinante para concluir o envio dos números.
2) Se houver necessidade, reduza o número total de números solicitados em um, envie o primeiro elemento da sequência de Fibonacci para o Assinante, ou seja, 0 e verifique novamente quantos outros elementos o Publisher pode nos fornecer, caso contrário, envie um sinal ao Assinante para concluir .
3) A mesma abordagem que no 2) parágrafo, mas apenas para o segundo elemento na sequência de Fibonacci.
4) Se forem necessários mais de 2 elementos, implementamos um algoritmo iterativo para encontrar números de Fibonacci, onde a cada passo transferiremos o próximo número da sequência de Fibonacci para o Assinante e também verificaremos quantos elementos o Publisher ainda pode fornecer. Se o Publisher não fornecer mais novos números, envie ao Assinante um sinal de conclusão.


No momento, escrevemos esse 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) { // 1 guard count > .none else { subscriber?.receive(completion: .finished) return } // 2 count -= .max(1) subscriber?.receive(0) if count == .none { subscriber?.receive(completion: .finished) return } // 3 count -= .max(1) subscriber?.receive(1) if count == .none { subscriber?.receive(completion: .finished) return } // 4 var prev = 0 var current = 1 var temp: Int while true { temp = prev prev = current current += temp subscriber?.receive(current) count -= .max(1) if count == .none { subscriber?.receive(completion: .finished) return } } } } 

Primeiro teste


Agora vamos testar o que temos, temos nosso editor e assinatura, não temos Sibscriber suficiente, a Combine fornece 2 Sibscriber da caixa: afundar e atribuir.


  • coletor - esse método cria um assinante e solicita imediatamente um número ilimitado de valores.
  • atribuir - define cada elemento do Publisher para uma propriedade de objeto.

pia é adequada para o nosso propósito, atenção especial deve ser dada ao fato de solicitar um número ilimitado de valores.


E aqui precisamos fazer uma distinção importante, nosso Publicador na variável count determina o número de elementos que nosso Publicador pode fornecer e essas condições são determinadas por nós mesmos. Em princípio, poderíamos ficar sem essa variável e não estarmos limitados na transmissão de números de Fibonacci, mas logo iríamos além da faixa de valores admissíveis do tipo Int.
O caso com coletor é diferente, cada Assinante determina quantos valores deseja receber, coletor solicita um número ilimitado de valores, o que significa que receberá valores até receber um sinal de conclusão, erro ou cancelamento.


Para a conveniência de usar nosso Publicador, adicionamos sua criação à extensão de protocolo do Publicador.


 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)) } } 

E então experimente nosso editor


 Publishers.fibonacci(count: .max(10)) .sink { value in print(value, terminator: " ") } // print 0 1 1 2 3 5 8 13 21 34 - OK 

E agora os casos de fronteira


 Publishers.fibonacci(count: .max(1)) .sink { value in print(value, terminator: " ") } // prinst 0 - OK Publishers.fibonacci(count: .max(2)) .sink { value in print(value, terminator: " ") } // prints 0 1 - OK Publishers.fibonacci(count: .none) .print() //    publisher'a .sink { value in print(value, terminator: " ") } // prints receive finished - OK 

Mas o que acontece se você especificar .unlimited?


 Publishers.fibonacci(count: .unlimited) .print() .sink { value in print(value, terminator: " ") } // prints 0 1 1 2 3 5 8 13 21 ...   ,     Int. 

Como você pode usar .unlimited, mas conseguir gerar vários números? Para fazer isso, precisamos do operador .prefix (_), que funciona da mesma maneira que o .prefix (_) das coleções, ou seja, ele deixa apenas os primeiros N elementos.


 Publishers.fibonacci(count: .unlimited) .print() .prefix(5) .sink { _ in } // prints 0 1 1 2 3 cancel   ,       Int. 

Qual é o problema? Talvez em .prefix (_)? Vamos fazer um pequeno experimento na sequência padrão da Foundation.


 //   1 2 3 4 5 6 7 8 ... 1... .publisher .print() .prefix(5) .sink { _ in } // prints 1 2 3 4 5 cancel -  

Como podemos ver, o código acima funcionou corretamente e o problema está em nossa implementação do Publisher.
Observamos os logs de .print () e vemos que, depois de N solicitações, de .prefix (_), chamamos cancel () em nossa FibonacciSubscription, onde definimos o assinante como nulo.


  func cancel() { subscriber = nil } 

Se você abrir a pilha de chamadas, poderá ver que cancel () é chamado a partir da solicitação (_ :), ou seja, durante a chamada para o assinante? .Receber (_). A partir do qual podemos concluir que em algum momento da solicitação (_ :) o assinante pode se tornar nulo e, em seguida, precisamos interromper o trabalho de gerar novos números. Adicione esta condição ao nosso código.


  func request(_ demand: Subscribers.Demand) { // 1 guard count > .none else { subscriber?.receive(completion: .finished) return } // 2 count -= .max(1) subscriber?.receive(0) guard let _ = subscriber else { return } // new if count == .none { subscriber?.receive(completion: .finished) return } // 3 count -= .max(1) subscriber?.receive(1) guard let _ = subscriber else { return } // new if count == .none { subscriber?.receive(completion: .finished) return } // 4 var prev = 0 var current = 1 var temp: Int while let subscriber = subscriber { // new temp = prev prev = current current += temp subscriber.receive(current) count -= .max(1) if count == .none { subscriber.receive(completion: .finished) return } } } 

Agora execute nosso código de teste.


 Publishers.fibonacci(count: .unlimited) .print() .prefix(5) .sink { _ in } // prints 0 1 1 2 3 cancel -  

Tem o comportamento esperado.


Assinante


E assim está pronta a nossa assinatura Fibonacci? Na verdade, em nossos testes, usamos apenas um assinante de coletor que solicita um número ilimitado de números, mas e se usarmos um assinante que esperará um determinado número limitado de números. A Combine não fornece esse assinante, mas o que nos impede de escrever nossos próprios? Abaixo está a implementação do nosso FibonacciSubscriber.


 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)") } } 

Portanto, nosso FibonacciSubscriber possui uma propriedade limit, que determina quantos elementos esse Assinante deseja receber. E isso é feito no método receive (_: Subscription), onde dizemos à assinatura quantos elementos precisamos. Também é importante notar a função Receive (_: Input) -> Subscribers.Demand, essa função é chamada quando um novo valor é recebido, como o valor de retorno, indicamos quantos elementos adicionais queremos receber: .none - de jeito nenhum, .max (N) N pieces , no total, o número total de elementos recebidos será igual à soma do valor da assinatura enviada em Receive (_: Subscription) e todos os valores de retorno de Receive (_: Input) -> Subscribers.Demand.


Segundo teste


Vamos tentar usar o FibonacciSubscriber.


 let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber) // prints 0 1 1 2 3 -     0 1 1 

Como vemos, nosso editor enviou 5 valores, em vez de 3. Por que? Como o método request (_: Subscribers.Demand) de FibonacciSubscription'a não leva em consideração as necessidades do assinante, vamos corrigi-lo. Para isso, adicionaremos uma propriedade adicional solicitada, através da qual rastrearemos as necessidades do assinante.


 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 // new 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 // new count -= .max(1) requested -= .max(1) // new requested += subscriber?.receive(0) ?? .none // new guard let _ = subscriber, requested > .none else { return } // new if count == .none { subscriber?.receive(completion: .finished) return } count -= .max(1) requested -= .max(1) // new requested += subscriber?.receive(1) ?? .none // new guard let _ = subscriber, requested > .none else { return } // new if count == .none { subscriber?.receive(completion: .finished) return } var prev = 0 var current = 1 var temp: Int while let subscriber = subscriber, requested > .none { // new temp = prev prev = current current += temp requested += subscriber.receive(current) // new count -= .max(1) requested -= .max(1) // new if count == .none { subscriber.receive(completion: .finished) return } } } } 

Terceiro teste


 let subscriber = FibonacciSubscriber(limit: .max(3)) Publishers.fibonacci(count: .max(5)) .print() .subscribe(subscriber) // prints 0 1 1 - OK 

O Publisher agora está funcionando corretamente.


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 artigo tenha lhe proporcionado uma melhor compreensão do que são Publisher, Subscription e Subscriber, como eles interagem entre si e em quais pontos você precisa prestar atenção ao decidir implementar o Publisher. Quaisquer comentários, esclarecimentos ao artigo são bem-vindos.

Source: https://habr.com/ru/post/pt482690/


All Articles