Salvando a lógica de negócios no Swift Combine. Parte 1

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 filtercomo map, drope scan. Além disso, apresentamos alguns operadores que conectam ( Zipe CombineLatest) ou unificam ( Mergee 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 StreamViewe StreamViewassina 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 JSONEncodere 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.

  1. Um fluxo não é a menor unidade em uma estrutura. O menor é o valor do fluxo.
  2. 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]
}

StreamIteminclui 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 valuee StreamItemcumpram 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 StreamItema StreamModel.

struct StreamModel<T: Codable>: Codable, Identifiable {
 var id: UUID
 var name: String?
 var description: String?
 var stream: [StreamItem<T>]
}

StreamModelcontém uma matriz de StreamItems. StreamModeltambém possui propriedades de identificador, nome e descrição. Novamente, tudo StreamModeldeve ser codificado para armazenamento e distribuição.

Crie uma estrutura de operador


Como mencionamos anteriormente, as operadoras delaypodem alterar o tempo de transmissão StreamItem.

enum Operator {
 case delay(seconds: Double)
}

Consideramos o operador delaycomo uma enumeração ( enum) com um valor associado para armazenar o tempo de atraso.

Obviamente, a enumeração Operatortambé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.

eu
et 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 eraseToAnyPublishere 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 appendpara 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 StreamItemcom 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".

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


All Articles