Uma introdução ao framework Combine

André Papoti de Oliveira
Academy@EldoradoCPS

--

Introduzido pela Apple no WWDC 2019, o Combine é um framework que fornece uma API para processar valores ao longo do tempo; Onde estes podem representar diversos tipos de eventos assíncronos. É uma ferramenta muito poderosa que pode simplicar seu código ao lidar com eventos de interface gerado pelo usuário, requests entre outros tipos de eventos assíncronos.

Neste framework, todos estes eventos assíncronos podem ser representados por Publishers e estruturas que dependem da informação contida nestes podem ser representadas por Subscribers e adquirem ela através de Subscriptions.

Uma analogia simples de se fazer é imaginar o Publisher como uma antena de rádio que, de maneira aleatória, emite ondas de rádio e os Subscribers como rádios. Estes, ao realizar uma Subscription, seria o equivalente ao configurar o rádio para escutar as ondas transmitidas pelo Publisher.

Publishers

É um protocolo que declara um tipo que emite uma sequência de valores ao longo do tempo e, eventualmente, pode se completar ou emitir um erro. Tem dois tipos associados:

  • Output: representa os valores que o Publisher emite para seus Subscribers, podem ser um valor (Int, Double, String, …)
  • Failure: Representa os tipos de erro que o Publisher pode emitir. É importante notar que, assim que o Publisher emite um erro, ele para de emitir valores

Existe uma classe especial de Publisher chamada de Subjects. Subjects se diferenciam de Publishers pois, para estas, podemos enviar valores manualmente. No framework Combine temos dois tipos de Subjects:

  • PassthroughSubject: Usado para representar eventos
  • CurrentValueSubject: Usado para representar estados

É possível transformar qualquer variável em um Publisher usando o property wrapper @Published e se modificamos esta imperativamente enviaremos uma notificação para todos

@Published var myVar: Int = 0myVar += 10 // Estaremos notificando todos subscribers

Subscribers

É um protocolo que declara um tipo que pode receber input de um Publisher, assim como estes, o Subscriber possui dois tipos associados

  • Input: Representa os valores que o Subscriber pode receber
  • Failure: Representa os tipos de erro que o Subscriber pode receber

O framework nos provém com dois Subscribers por padrão:

  • sink(receiveCompletion:receiveValue:): recebe duas closures, a primeira é executada quando o Publisher emite uma Failure e a segunda quando recebe um elemento do Publisher
  • assign(to:on:): atribui cada elemento que recebe para uma determinada propriedade de um objeto, indicado por um keypath

Utilizando o exemplo anterior, podemos criar um Subscriber que será notificado toda vez que o valor de myVar for alterado

let sub = $myVar
.sink(reciveCompletion: {},
reciveValue: {print("\($0)")})

Subscriptions

Representam a conexão entre um Publisher e um Subscriber. É responsabilidade do Publisher de prover uma Subscription para o Subscriber através do método receive(subscription:).

Para que ela seja valida algumas regras devem ser obedecidas:

  • Um Subscriber pode ter apenas uma subscription
  • No máximo uma Failure será emitida pelo Publisher
  • Tanto o Input e o Output quanto ambos Failures devem ser do mesmo tipo.

Porém, para que esta última regra seja respeitada podemos utilizar de operadores oferecidos pelo Combine. Mesmo que o Output não tenha o mesmo tipo que o Input podemos utilizar estes operadores para que, assim que a informação chegue ao subscriber, esta esteja no formato correto

Vamos pegar o exemplo anterior e fazer com que apenas valores maiores que 5 sejam mostrados no console:

let sub = $myVar
.map{$0 > 5}
.sink(reciveCompletion: {},
reciveValue: {print("\($0)")})

Como utilizar estes conceitos na prática

Para exemplificar todos os conceitos que foram explicados, vamos criar um playground bem simples que vai simular uma estação de rádio enviando dados de música para um rádio que contém um display que exibe o nome da música

Primeiro, vamos criar a estrutura que será utilizada para representar as músicas:

struct Song {
var author: String
var title: String
var durationInSeconds: Int
}

Em seguida nosso rádio (Subject) usando um PassthroughSubject que envia objetos do tipo Song para seus subscribers:

enum StationError: Error {
case finishBroadcast
}
let radioStation = PassthroughSubject<Song, StationError>()

E por fim o nosso rádio (Subscriber) utilizando o subscriber padrão sink(receiveCompletion:receiveValue:):

let radioDisplay = radioStation
.map{$0.title}
.sink(receiveCompletion: {error in
switch
error {
case .failure(.finishBroadcast):
print("Transmissão encerrada")
default:
print("Erro desconhecido")
}
},
receiveValue: {value in
print("Nome da música: \(value)")
})

Como queremos que apenas o titulo da música seja exibido no nosso display utilizamos do operador map{} para transformar o objeto Song recebido em apenas uma String que contem o nome da música

Agora vamos testar. Primeiro vamos criar um array de Songs

let songList = [
Song(author: "Tim Maia", title: "Descobridor do sete mares", durationInSeconds: 228),
Song(author: "Jack Stauber", title: "Two Time", durationInSeconds: 154),
Song(author: "NICO", title: "Diver", durationInSeconds: 213)]

E pedir para que o Subject envie os elementos do array um a um e, por fim, finalize a transmissão

for song in songList {
radioStation.send(song)
}
radioStation.send(completion: .failure(.finishBroadcast))

O output do playground será o seguinte

Nome da música: Descobridor dos sete mares
Nome da música: Two Time
Nome da música: Diver
Transmissão encerrada

Combine e SwiftUI

Outra novidade da Apple é o SwiftUI. Introduzido também no WWDC 2019, o SwiftUI é um framework que disponibiliza uma API que permite criar interfaces de forma declarativa e que funcionam em todas plataformas da Apple.

Por conta da maior flexibilidade e facilidade de desenvolvimento, nos próximos anos, este framework deve se tornar o principal para a criação de interfaces para aplicações nativas.

Mas o que isso tem a ver com Combine? É muito simples, ambos frameworks estão profundamente ligados. Por baixo dos panos o SwiftUI utiliza de muitas features do Combine para ativamente atualizar todas as suas views e saber quando recarregá-las

Se pegarmos a implementação de ObservableObject, utilizado para notificar a interface que o estado de um objeto mudou, por exemplo:

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)public protocol ObservableObject : AnyObject {
/// The type of publisher that emits before the object has changed.
associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never/// A publisher that emits before the object has changed.
var objectWillChange: Self.ObjectWillChangePublisher { get }
}

Podemos ver que ObjectWillChangePublisher é um Publisher, e isso se repete para diversas outras implementações do framework. Por conta disso, fica muito fácil de se utilizar as features do Combine para atualizar uma interface construída com SwiftUI

--

--