Combinação orientada a dados
A tradução do artigo foi preparada especialmente para os alunos do curso avançado "iOS Developer" .
Nas séries anteriores de postagens, construímos com sucesso uma plataforma sobre o SwiftUI, com a qual você pode observar livremente a sequência de valores que passam pelo editor Combine.Também criamos uma série de exemplos demonstrando vários operadores padrão da Combine capazes de modificar e converter valores em sequências filter
como map
, drop
e scan
. Além disso, apresentamos alguns operadores que conectam ( Zip
e CombineLatest
) ou unificam ( Merge
e Append
) a sequência.Nesse ponto, alguns de vocês podem estar cansados de ter que organizar e manter tanto código para cada exemplo (pelo menos já estou cansado). Veja quantos deles estão no repositório combine-magic-swiftui na pasta do tutorial? Cada um dos exemplos é uma representação do SwiftUI. Cada um deles simplesmente transfere um ou vários editores para StreamView
e StreamView
assina editores com o clique de um botão.Portanto, devo ser capaz de gerar programaticamente uma lista de editores na inicialização e reutilização do aplicativo StreamView
, como na captura de tela abaixo.
No entanto, o problema com esta solução é a escalabilidade, quando você precisa criar muitos editores.Minha solução para esse problema é armazenar de alguma forma esses editores. Se eu puder serializá-los de alguma forma, posso salvá-los. Se eu conseguir salvá-los, não só posso modificá-los sem alterar o código, mas também posso compartilhá-los com outros dispositivos compatíveis com o Combine .Armazenamento e transferência de operadores Combine
Agora, vejamos nossos objetivos aqui mais especificamente. Como temos uma lista de fluxos e operadores em um formato Publisher
, gostaríamos de poder salvá-los em qualquer tipo de armazenamento - por exemplo, em um disco rígido ou em um banco de dados.Obviamente, também precisamos converter os dados armazenados de volta para o editor, mas, além disso, queremos trocar, transferir e distribuir esses editores com operadores de um lugar para outro.Depois de configurarmos essa estrutura, como você já deve ter adivinhado, em um ambiente distribuído, um serviço centralizado poderá começar a gerenciar a lógica computacional de um grupo de clientes.
Estrutura codificável
Então, como fazemos isso? Começaremos desenvolvendo uma estrutura que é serializável e desserializável. O protocolo Swift Codable
nos permite fazer isso através de JSONEncoder
e JSONDecoder
. Além disso, a estrutura deve representar corretamente dados e comportamentos para a menor unidade de valor no fluxo, até cadeias complexas de operadores.Antes de prosseguir para entender quais componentes são necessários para as estruturas que vamos criar, vamos relembrar o fluxo principal que criamos na série anterior de postagens .Fluxo de números
Este é o fluxo mais fácil; no entanto, se você olhar mais profundamente, notará que essa não é apenas uma sequência de matrizes. Cada um dos blocos redondos possui seu próprio operador de atraso ( delay
), que determina o tempo real em que deve ser transmitido. Cada valor em Combine fica assim:Just(value).delay(for: .seconds(1), scheduler: DispatchQueue.main)
E, em geral, tudo se parece com isso:let val1 = Just(1).delay(for: .seconds(1), scheduler: DispatchQueue.main)
let val2 = Just(2).delay(for: .seconds(1), scheduler: DispatchQueue.main)
let val3 = ....
let val4 = ....
let publisher = val1.append(val2).append(val3).append(val4)
Cada valor está atrasado por um segundo e a mesma instrução é adicionada ao próximo valor delay
.Portanto, aprendemos duas coisas com nossas observações.- Um fluxo não é a menor unidade em uma estrutura. O menor é o valor do fluxo.
- Cada valor de fluxo pode ter operadores ilimitados que controlam quando e qual valor é transmitido.
Crie seu StreamItem
Como o valor do fluxo e de seus operadores é a menor unidade, começamos criando sua estrutura. Vamos ligar para ela StreamItem
.struct StreamItem<T: Codable>: Codable {
let value: T
var operators: [Operator]
}
StreamItem
inclui o valor do fluxo e uma matriz de operadores. De acordo com nossos requisitos, queremos ser capazes de preservar tudo na estrutura para que ambos value
e StreamItem
cumpram o protocolo Codable
.O valor do fluxo deve ser universal para acomodar qualquer tipo de valor.Crie seu StreamModel
Discutiremos a estrutura para os operadores mais tarde. Vamos conectar a matriz StreamItem
a StreamModel
.struct StreamModel<T: Codable>: Codable, Identifiable {
var id: UUID
var name: String?
var description: String?
var stream: [StreamItem<T>]
}
StreamModel
contém uma matriz de StreamItem
s. StreamModel
também possui propriedades de identificador, nome e descrição. Novamente, tudo StreamModel
deve ser codificado para armazenamento e distribuição.Crie uma estrutura de operador
Como mencionamos anteriormente, as operadoras delay
podem alterar o tempo de transmissão StreamItem
.enum Operator {
case delay(seconds: Double)
}
Consideramos o operador delay
como uma enumeração ( enum
) com um valor associado para armazenar o tempo de atraso.Obviamente, a enumeração Operator
também deve corresponder Codable
, o que inclui valores relacionados à codificação e decodificação. Veja a implementação completa abaixo.enum Operator {
case delay(seconds: Double)
}
extension Operator: Codable {
enum CodingKeys: CodingKey {
case delay
}
struct DelayParameters: Codable {
let seconds: Double
}
enum CodingError: Error { case decoding(String) }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let delayParameters = try? container.decodeIfPresent(DelayParameters.self, forKey: .delay) {
self = .delay(seconds: delayParameters.seconds)
return
}
throw CodingError.decoding("Decoding Failed. \(dump(container))")
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .delay(let seconds):
try container.encode(DelayParameters(seconds: seconds), forKey: .delay)
}
}
}
Agora temos uma boa estrutura para representar esse fluxo seqüencial, que gera valores de 1 a 4 com um segundo intervalo de atraso.euet streamA = (1...4).map { StreamItem(value: $0,
operators: [.delay(seconds: 1)]) }
let serialStreamA = StreamModel(id: UUID(), name: "Serial Stream A",
description: nil, stream: streamA)
Conversão StreamModel para Publisher
Agora, criamos uma instância do fluxo; no entanto, se não o convertermos em um editor, tudo não terá sentido. Vamos tentar.Antes de tudo, cada modelo de operador refere-se ao operador Combine real, que deve ser adicionado a este editor e devolvido ao editor operado.extension Operator {
func applyPublisher<T>(_ publisher: AnyPublisher<T, Never>) -> AnyPublisher<T, Never> {
switch self {
case .delay(let seconds):
return publisher.delay(for: .seconds(seconds), scheduler: DispatchQueue.main).eraseToAnyPublisher()
}
}
}
No momento, existe apenas um tipo de operador - delay
. Adicionaremos mais à medida que avançamos.Agora podemos começar a usar editores para todos StreamItem
.extension StreamItem {
func toPublisher() -> AnyPublisher<T, Never> {
var publisher: AnyPublisher<T, Never> =
Just(value).eraseToAnyPublisher()
self.operators.forEach {
publisher = $0.applyPublisher(publisher)
}
return publisher
}
}
Começamos com o valor Just
, generalizamos usando o método eraseToAnyPublisher
e depois usamos os editores de todos os operadores relacionados.No nível StreamModel
, obtemos o editor de todo o fluxo.extension StreamModel {
func toPublisher() -> AnyPublisher<T, Never> {
let intervalPublishers =
self.stream.map { $0.toPublisher() }
var publisher: AnyPublisher<T, Never>?
for intervalPublisher in intervalPublishers {
if publisher == nil {
publisher = intervalPublisher
continue
}
publisher =
publisher?.append(intervalPublisher).eraseToAnyPublisher()
}
return publisher ?? Empty().eraseToAnyPublisher()
}
}
Você adivinhou certo: usamos o método append
para combinar editores.Visualização, edição e novamente visualização de um fluxo
Agora podemos simplesmente decodificar o editor, transferir e criar um StreamView (veja como fizemos isso nas postagens anteriores ). E por último, mas não menos importante: agora podemos simplesmente editar StreamModel
, adicionar outros StreamItem
com novos valores e até compartilhar esse modelo com outros dispositivos via Internet.Veja a demonstração abaixo. Agora podemos fazer alterações no fluxo sem alterar o código.
Próximo capítulo: Serializando / desserializando filtros e operadores de mapa
Na próxima parte, adicionaremos mais operadores à enumeração do operador e começaremos a aplicá-los no nível do encadeamento.Até a próxima vez, você poderá encontrar o código-fonte aqui neste repositório combine-magic-swifui na pasta combine-playground.Estamos aguardando seus comentários e convidamos você para um seminário on- line aberto sobre o tópico "Aplicativo iOS no SwiftUI usando o Kotlin Mobile Multiplatform".