Guardando la lógica de negocios en Swift Combine. Parte 1

Combinación orientada a datos





La traducción del artículo fue preparada especialmente para estudiantes del curso avanzado "Desarrollador iOS" .





En la serie anterior de publicaciones, creamos con éxito una plataforma sobre SwiftUI, con la que puede observar libremente la secuencia de valores que pasan a través del editor Combine.

También creamos una serie de ejemplos que demuestran varios operadores predeterminados de Combinar que son capaces de modificar y convertir valores en secuencias filtercomo map, dropy scan. Además, hemos presentado algunos operadores que conectan ( Zipy CombineLatest) o unifican ( Mergey Append) la secuencia.

En este punto, algunos de ustedes podrían estar cansados ​​de tener que organizar y mantener tanto código para cada ejemplo (al menos ya estoy cansado). ¿Ves cuántos de ellos están en el repositorio combine-magic-swiftui en la carpeta del tutorial? Cada uno de los ejemplos es una representación de SwiftUI. Cada uno de ellos simplemente transfiere uno o varios editores a StreamView, y StreamViewfirma editores con el clic de un botón.

Por lo tanto, debería poder generar mediante programación una lista de editores al inicio y reutilización de la aplicación StreamView, como en la siguiente captura de pantalla.



Sin embargo, el problema con esta solución es la escalabilidad, cuando necesita crear muchos editores.

Mi solución a este problema es almacenar de alguna manera a estos editores. Si puedo serializarlos de alguna manera, puedo guardarlos. Si logro guardarlos, no solo puedo modificarlos sin cambiar el código, sino que también puedo compartirlos con otros dispositivos que admitan Combine .

Almacenamiento y transferencia de operadores Combine


Ahora veamos nuestros objetivos aquí más específicamente. Dado que tenemos una lista de transmisiones y operadores en un formato Publisher, nos gustaría poder guardarlos en cualquier tipo de almacenamiento, por ejemplo, en un disco duro o en una base de datos.

Obviamente, también debemos poder convertir los datos almacenados de nuevo al editor, pero además, queremos poder intercambiar, transferir y distribuir estos editores con operadores de un lugar a otro.

Después de configurar dicha estructura, como habrás adivinado, en un entorno distribuido, un servicio centralizado puede comenzar a administrar la lógica computacional para un grupo de clientes.



Estructura codificable


¿Entonces como hacemos esto? Comenzaremos desarrollando una estructura que sea serializable y deserializable. El protocolo Swift Codable nos permite hacer esto a través de JSONEncodery JSONDecoder. Además, la estructura debe representar correctamente datos y comportamientos para la unidad de valor más pequeña en la secuencia, hasta cadenas complejas de operadores.

Antes de continuar para comprender qué componentes son necesarios para las estructuras que vamos a crear, recordemos la secuencia principal que creamos en la serie anterior de publicaciones .

Flujo de números




Esta es la secuencia más fácil; sin embargo, si observa más a fondo, notará que esto no es solo una secuencia de matrices. Cada uno de los bloques redondos tiene su propio operador de retardo ( delay), que determina el tiempo real en que debe transmitirse. Cada valor en Combinar se ve así:

Just(value).delay(for: .seconds(1), scheduler: DispatchQueue.main)

Y en general, todo se ve así:

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 se retrasa por un segundo y se agrega la misma declaración al siguiente valor delay.

Por lo tanto, aprendemos dos cosas de nuestras observaciones.

  1. Una secuencia no es la unidad más pequeña de una estructura. El más pequeño es el valor de la secuencia.
  2. Cada valor de flujo puede tener operadores ilimitados que controlan cuándo y qué valor se transmite.

Crea tu StreamItem


Dado que el valor de la secuencia y sus operadores son la unidad más pequeña, comenzamos creando su estructura. Vamos a la llaman StreamItem.

struct StreamItem<T: Codable>: Codable {
 let value: T
 var operators: [Operator]
}

StreamItemincluye el valor de la secuencia y una matriz de operadores. De acuerdo con nuestras necesidades, queremos ser capaces de preservar todo en la estructura de manera que ambos value, y StreamItemcumplir con el protocolo Codable.

El valor del flujo debe ser universal para acomodar cualquier tipo de valor.

Crea tu StreamModel


Discutiremos la estructura para los operadores más adelante. Vamos a conectar la matriz StreamItema StreamModel.

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

StreamModelcontiene una matriz de StreamItems. StreamModeltambién tiene propiedades de identificación, nombre y descripción. Nuevamente, todo StreamModeldebe ser codificable para almacenamiento y distribución.

Crear una estructura de operador


Como mencionamos anteriormente, los operadores delaypueden cambiar el tiempo de transmisión StreamItem.

enum Operator {
 case delay(seconds: Double)
}

Consideramos al operador delaycomo una enumeración ( enum) con un valor asociado para almacenar el tiempo de retraso.

Por supuesto, la enumeración Operatortambién debe coincidir Codable, lo que incluye codificar y decodificar valores relacionados. Vea la implementación completa a continuación.

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

}

Ahora tenemos una buena estructura para representar esta secuencia secuencial, que genera valores de 1 a 4 con un segundo intervalo de retraso.

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

Convierta StreamModel a Publisher


Ahora hemos creado una instancia de la secuencia; sin embargo, si no lo convertimos en un editor, todo no tendrá sentido. Intentemos.

En primer lugar, cada modelo de operador se refiere al operador Combine real, que debe agregarse a este editor y devolverse al 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()
  }
 }
}

Por el momento solo hay un tipo de operador - delay. Añadiremos más a medida que avanzamos.

Ahora podemos comenzar 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
}
}

Comenzamos con el valor Just, lo generalizamos usando el método eraseToAnyPublishery luego usamos los editores de todos los operadores relacionados.

En el nivel StreamModeltenemos al editor de toda la transmisión.

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

Lo has adivinado bien: utilizamos el método appendpara combinar editores.

Visualización, edición y nuevamente visualización de una secuencia.


Ahora podemos simplemente decodificar el editor, transferir y crear un StreamView (vea cómo lo hicimos en publicaciones anteriores ). Y por último, pero no menos importante: ahora podemos simplemente editar StreamModel, agregar StreamItemvalores adicionales con nuevos valores e incluso compartir este modelo con otros dispositivos a través de Internet.

Vea la demostración a continuación. Ahora podemos hacer cambios en la transmisión sin cambiar el código.



Siguiente capítulo: Serialización / Deserialización de filtros y operadores de mapas


En la siguiente parte, agregaremos más operadores a la enumeración de operadores y comenzaremos a aplicarlos a nivel de hilo.

Hasta la próxima, puede encontrar el código fuente aquí en este repositorio combine-magic-swifui en la carpeta combine-playground.

Estamos a la espera de sus comentarios y estamos invitados a un seminario web abierto sobre el tema "Aplicación de iOS en SwiftUI usando Kotlin Mobile Multiplatform".

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


All Articles