
Há um ano e meio,
cantei os louvores do RxSwift . Demorei um pouco para descobrir, mas quando isso aconteceu, não havia como voltar atrás. Agora eu tinha o melhor martelo do mundo e me amaldiçoasse se tudo ao meu redor não parecesse um prego.
A Apple apresentou a estrutura
Combine na
WWDC Summer Conference. À primeira vista, parece uma versão um pouco melhor do RxSwift. Antes que eu possa explicar o que gosto e o que não, precisamos entender qual problema o Combine foi projetado para resolver.
Programação reativa? E daí?
A comunidade ReactiveX - da qual a comunidade RxSwift faz parte - explica sua essência da seguinte maneira:
API para programação assíncrona com threads observáveis.
E também:
O ReactiveX é uma combinação das melhores idéias dos padrões de design do Observer e Iterator, além de programação funcional.
Bem ... tudo bem.
E o que isso
realmente significa?
O básico
Para realmente entender a essência da programação reativa, acho útil entender como chegamos a ela. Neste artigo, descreverei como você pode examinar os tipos existentes em qualquer linguagem OOP moderna, alterá-los e chegar à programação reativa.
Neste artigo, mergulharemos rapidamente na selva, o que não é
absolutamente necessário para entender a programação reativa.
No entanto, considero isso um curioso exercício acadêmico, especialmente em termos de quão fortemente tipadas as línguas podem nos levar a novas descobertas.
Então, aguarde meus próximos posts, se você estiver interessado em novos detalhes.
Enumerável
A “
programação reativa ” conhecida por mim se originou na linguagem em que escrevi uma vez - C #. A premissa em si é bastante simples:
E se, em vez de extrair valores de enumeráveis, eles enviarem os valores por você?Essa idéia, "empurrar em vez de puxar", foi melhor
descrita por Brian Beckman e Eric Meyer. Os primeiros 36 minutos ... eu não entendi nada, mas
a partir do 36º minuto isso se torna
realmente interessante.
Em resumo, vamos reformular a idéia de um grupo linear de objetos no Swift, bem como um objeto que pode interagir com esse grupo linear. Você pode fazer isso definindo estes protocolos Swift falsos:
Duplas
Vamos virar tudo e fazer
duplas . Enviaremos dados para onde eles vieram. E obtenha os dados de onde eles saíram. Parece absurdo, mas aguente um pouco.
Enumerável Duplo
Vamos começar com Enumerable:
Como
getEnumerator()
pegou o
Void
e deu o
Enumerator
, agora aceitamos o [double]
Enumerator
e fornecemos o
Void
.
Eu sei que isso é estranho. Não saia.
Enumerador duplo
E então o que é
DualOfEnumerator
?
Existem vários problemas aqui:
- Não há conceito de uma propriedade somente de conjunto no Swift.
- O que aconteceu com os
throws
em Enumerator.moveNext()
?
- O que acontece com
Disposable
?
Para corrigir o problema com a propriedade set-only, podemos tratá-lo como o que realmente é - uma função. Vamos
DualOfEnumerator
nosso
DualOfEnumerator
:
protocol DualOfEnumerator {
Para resolver o problema com
throws
, vamos separar o erro que pode ocorrer em
moveNext()
e trabalhar com ele como uma função
error()
separada:
protocol DualOfEnumerator {
Podemos fazer outra coisa: dê uma olhada na assinatura da conclusão da iteração:
func enumeratorIsDone(Bool)
Provavelmente algo semelhante acontecerá com o tempo:
enumeratorIsDone(false) enumeratorIsDone(false)
Agora, vamos simplificar as coisas e chamar
enumeratorIsDone
somente quando ... tudo estiver realmente pronto. Guiados por essa idéia, simplificamos o código:
protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) }
Cuide-se
E quanto ao
Disposable
? O que fazer com isso? Como
Disposable
faz parte
do tipo Enumerator
, quando obtemos o
Enumerator
dobro , provavelmente não deveria estar no
Enumerator
. Em vez disso, ele deve fazer parte do
DualOfEnumerable
. Mas onde exatamente?
DualOfEnumerator
aqui:
func subscribe(DualOfEnumerator)
Se aceitarmos o
DualOfEnumerator
, o
Disposable
não deve ser
retornado ?
Aqui está o tipo de dobro que você recebe no final:
protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) }
Chame de rosa, embora não
Então, mais uma vez, eis o que temos:
protocol DualOfEnumerable { func subscribe(DualOfEnumerator) -> Disposable } protocol DualOfEnumerator { func enumeratorIsDone() func error(Error) func next(Element) }
Vamos brincar um pouco com os nomes agora.
Vamos começar com o
DualOfEnumerator
. Criaremos nomes melhores para as funções para descrever com mais precisão o que está acontecendo:
protocol DualOfEnumerator { func onComplete() func onError(Error) func onNext(Element) }
Muito melhor e mais compreensível.
E os nomes dos tipos? Eles são simplesmente terríveis. Vamos mudar um pouco.
DualOfEnumerator
- algo que segue o que acontece com um grupo linear de objetos. Podemos dizer que ele observa um grupo linear.
DualOfEnumerable
é um assunto de observação. O que estamos assistindo. Portanto, pode ser chamado de observável .
Agora faça as alterações finais e obtenha o seguinte:
protocol Observable { func subscribe(Observer) → Disposable } protocol Observer { func onComplete() func onError(Error) func onNext(Element) }
Uau
Acabamos de criar dois objetos fundamentais no RxSwift. Você pode ver suas versões reais
aqui e
aqui . Observe que, no caso do Observer, as três funções
on()
são combinadas em uma
on(Event)
, onde
Event
é uma enumeração que determina qual é o evento - conclusão, próximo valor ou erro.
Esses dois tipos estão subjacentes ao RxSwift e à programação reativa.
Sobre protocolos falsos
Os dois protocolos "falsos" que mencionei acima não são realmente falsos. Estes são análogos de tipos existentes no Swift:
E daí?
Então, com o que se preocupar?
Tanto no desenvolvimento moderno -
especialmente no desenvolvimento de aplicativos - está associado à assincronia. O usuário clicou de repente em um botão. O usuário selecionou repentinamente uma guia no UISegmentControl. O usuário selecionou repentinamente uma guia na UITabBar. De repente, o soquete da Web nos deu novas informações. Este download de repente - e finalmente - terminou. Essa tarefa em segundo plano terminou abruptamente. Esta lista continua e continua.
No mundo moderno do CocoaTouch, existem muitas maneiras de lidar com esses eventos:
- notificações
- retornos de chamada
- Observação de valor-chave (KVO),
- mecanismo de alvo / ação.
Imagine se
tudo pudesse ser refletido em uma única interface. O que poderia funcionar com
qualquer tipo de dados ou eventos assíncronos em todo o aplicativo.
Agora imagine se houvesse todo um
conjunto de funções que permita modificar esses
fluxos , convertê-los de um tipo para outro, extrair informações dos Elements ou até combiná-las com outros fluxos.
De repente, em nossas mãos está um novo conjunto
universal de ferramentas.
E assim, voltamos ao começo:
API para programação assíncrona com threads observáveis.
É isso que faz do RxSwift uma ferramenta tão poderosa. Como Combinar.
O que vem a seguir?
Se você quiser ler mais sobre o RxSwift
na prática , recomendo
meus cinco artigos escritos em 2016 . Eles descrevem a criação de um aplicativo CocoaTouch simples, seguido por uma conversão em fases para o RxSwift.
Em um dos artigos a seguir, explicarei por que muitas das técnicas descritas em minha
série de artigos para iniciantes não são aplicáveis no Combine e também comparo o Combine ao RxSwift.
Combinar: qual é o objetivo?
A discussão do Combine também inclui uma discussão das principais diferenças entre ele e o RxSwift. Para mim, existem três deles:
- a possibilidade de usar classes não reativas,
- tratamento de erros
- contrapressão.
Vou dedicar um artigo separado para cada item. Vou começar com o primeiro.
Recursos do RxCocoa
Em
um post anterior, eu disse que o RxSwift é mais do que ... RxSwift. Ele oferece inúmeras possibilidades para o uso de controles do UIKit no subprojeto de tipo mas não muito do RxCocoa. Além disso, o
RxSwiftCommunity foi além e implementou muitas ligações para ruas
secundárias ainda mais isoladas do UIKit, além de outras classes CocoaTouch que o RxSwift e o RxCocoa ainda não cobrem.
Portanto, é muito fácil obter um fluxo
Observable
clicando, por exemplo, no UIButton. Vou dar este exemplo novamente:
let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag)
Peso leve.
Vamos (finalmente) ainda falar sobre Combinar
Combinar é muito semelhante ao RxSwift. Como a documentação diz:
A estrutura Combine fornece uma API Swift declarativa para manipular valores ao longo do tempo.
Parece familiar: lembre-se da descrição do ReactiveX (o projeto pai do RxSwift):
API para programação assíncrona com threads observáveis.
Nos dois casos, a mesma coisa é dita. É apenas que termos específicos são usados na descrição do ReactiveX. Pode ser reformulado da seguinte maneira:
Uma API para programação assíncrona com valores ao longo do tempo.
Quase o mesmo que para mim.
O mesmo de antes
Quando comecei a analisar a API, ficou óbvio imediatamente que a maioria dos tipos que conheço do RxSwift tem opções semelhantes no Combine:
- Observável → Editor
- Observador → Assinante
- Descartável → Cancelável . Este é um triunfo do marketing. Você não pode imaginar quantos olhares surpresos recebi de desenvolvedores mais imparciais quando comecei a descrever o Disposable no RxSwift.
- SchedulerType → Scheduler
Até agora tudo bem. Mais uma vez, gosto muito mais do cancelável do que do descartável. Um ótimo substituto, não apenas em termos de marketing, mas também em termos de uma descrição precisa da essência do objeto.
Mais é ainda melhor!
- Driver do RxCocoa -> Este é o BindableObject da SwiftUI
Isso não está claro de imediato, mas espiritualmente eles servem a um propósito, e nenhum deles pode gerar erros.
"Pausa para cocô"
Tudo muda assim que você começa a se aprofundar no RxCocoa. Lembre-se do exemplo acima, no qual desejamos obter um fluxo Observable que represente cliques no UIButton? Aqui está:
let disposeBag = DisposeBag() let button = UIButton() button.rx.tap .subscribe(onNext: { _ in print("Tap!") }) .disposed(by: disposeBag)
Combinar exige ... muito mais trabalho para fazer o mesmo.
Combinar não fornece nenhum recurso para ligação a objetos UIKit.Isso é ... apenas uma chatice irreal.
Aqui está uma maneira comum de obter
UIControl.Event de
UIControl usando Combine:
class ControlPublisher<T: UIControl>: Publisher { typealias ControlEvent = (control: UIControl, event: UIControl.Event) typealias Output = ControlEvent typealias Failure = Never let subject = PassthroughSubject<Output, Failure>() convenience init(control: UIControl, event: UIControl.Event) { self.init(control: control, events: [event]) } init(control: UIControl, events: [UIControl.Event]) { for event in events { control.addTarget(self, action: #selector(controlAction), for: event) } } @objc private func controlAction(sender: UIControl, forEvent event: UIControl.Event) { subject.send(ControlEvent(control: sender, event: event)) } func receive<S>(subscriber: S) where S : Subscriber, ControlPublisher.Failure == S.Failure, ControlPublisher.Output == S.Input { subject.receive(subscriber: subscriber) } }
Aqui ...
muito mais trabalho. Pelo menos a ligação se parece com:
ControlPublisher(control: self.button, event: .touchUpInside) .sink { print("Tap!") }
Para comparação, o RxCocoa fornece um cacau agradável e saboroso na forma de ligações a objetos UIKit:
self.button.rx.tap .subscribe(onNext: { _ in print("Tap!") })
Por si mesmos, esses desafios são realmente muito semelhantes. A única coisa frustrante é que eu tive que escrever o
ControlPublisher
para chegar a esse ponto. Além disso, o RxSwift e o RxCocoa são muito bem testados e são usados em projetos muito mais que os meus.
Para comparação, meu
ControlPublisher
apareceu apenas ... agora. Somente por causa do número de clientes (zero) e do tempo de uso no mundo real (quase zero em comparação com o RxCocoa) meu código pode ser considerado infinitamente mais perigoso.
Que chatice.
Ajuda da comunidade?
Honestamente, nada impede a comunidade de criar seu próprio código aberto “CombineCocoa”, o que preencheria a lacuna do RxCocoa, assim como a Comunidade RxSwift.
No entanto, considero isso uma grande desvantagem do Combine. Não quero reescrever o RxCocoa inteiro, apenas para obter ligações aos objetos UIKit.
Se eu decidir apostar no
SwiftUI , acho que isso
eliminará o problema da falta de vínculos. Até
meu pequeno aplicativo contém um
monte de código
de interface
do usuário. Jogar tudo isso apenas para pular no trem Combine seria pelo menos estúpido ou até perigoso.
A propósito, o artigo na documentação
Recebendo e manipulando eventos com Combine descreve brevemente como receber e processar eventos no Combine. A introdução é boa, mostra como extrair um valor de um campo de texto e salvá-lo em um objeto de modelo personalizado. A documentação também demonstra o uso de operadores para realizar algumas modificações mais avançadas no fluxo em questão.
Exemplo
Vamos para o final da documentação, onde o exemplo de código é:
let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField) .map( { ($0.object as! NSTextField).stringValue } ) .assign(to: \MyViewModel.filterString, on: myViewModel)
Eu tenho ... muitos problemas com isso.
Notificá-lo que eu não gosto
As duas primeiras linhas me causam mais perguntas:
let sub = NotificationCenter.default .publisher(for: NSControl.textDidChangeNotification, object: filterField)
O NotificationCenter é como um barramento de aplicativos (ou mesmo um barramento do sistema) no qual muitos podem lançar dados ou coletar informações que passam rapidamente. Esta solução é da categoria tudo para todos, conforme pretendido pelos criadores. E realmente existem muitas situações em que você pode precisar descobrir, digamos, que o teclado acabou de ser mostrado ou oculto. O NotificationCenter é uma ótima maneira de distribuir essa mensagem pelo sistema.
Mas para mim o
NotificationCenter é um
código com um estrangulamento . Há momentos (como obter uma notificação sobre o teclado) quando o NotificationCenter é realmente a
melhor solução
possível para o problema. Mas muitas vezes para mim o NotificationCenter é a solução
mais conveniente . É realmente muito conveniente soltar algo no NotificationCenter e buscá-lo em outro lugar do aplicativo.
Além disso, o NotificationCenter é do tipo
"string" , ou seja, você pode facilmente cometer o erro de qual notificação está tentando publicar ou ouvir. Swift está fazendo todo o possível para melhorar a situação, mas ainda assim o mesmo NSString ainda está oculto.
Sobre a KVO
Na plataforma da Apple, existe uma maneira popular de receber notificações de alterações em diferentes partes do código: observação de valor-chave (KVO). A Apple descreve assim:
Este é um mecanismo que permite que objetos recebam notificações de alterações nas propriedades especificadas de outros objetos.
Graças a um
tweet de Gui Rambo, notei que a Apple adicionou as ligações do KVO ao Combine. Isso poderia significar que eu poderia me livrar das muitas decepções com a falta de um análogo do RxCocoa no Combine. Se eu puder usar o KVO, isso provavelmente eliminará a necessidade do CombineCocoa, por assim dizer.
Tentei descobrir um exemplo de uso do KVO para obter um valor de um
UITextField
e enviá-lo para o console:
let sub = self.textField.publisher(for: \UITextField.text) .sink(receiveCompletion: { _ in print("Completed") }, receiveValue: { print("Text field is currently \"\($0)\"") })
Parece bom, seguir em frente?
Não é tão rápido, amigos.
Eu esqueci a
verdade desconfortável :
O UIKit, em geral, não é compatível com o KVO.
E sem o suporte da KVO, minha ideia não funcionará. Minhas verificações confirmaram o seguinte: o código não gera nada para o console quando insiro texto no campo.
Portanto, minhas esperanças de se livrar da necessidade de ligações do UIKit eram lindas, mas não por muito tempo.
Limpeza
Outro problema da Combine é que ainda não está claro onde e como liberar recursos em objetos
canceláveis .
Parece que devemos armazená-los em variáveis de instância. Mas não me lembro que na documentação oficial algo foi dito sobre a limpeza.
O RxSwift possui um DisposeBag terrivelmente nomeado, mas incrivelmente conveniente. Não é menos fácil criar o CancelBag no Combine, mas não tenho certeza de que, nesse caso, seja a melhor solução.
No
próximo artigo , falaremos sobre o tratamento de erros no RxSwift e Combine, sobre as vantagens e desvantagens de ambas as abordagens.