Administrador de visualización de datos reactivos. Introduccion

Esta es la primera parte de una serie de artículos sobre la biblioteca ReactiveDataDisplayManager (RDDM) . En este artículo, describiré los problemas comunes con los que tengo que lidiar cuando trabajo con tablas "regulares", y daré una descripción de RDDM.




Problema 1. UITableViewDataSource


Para empezar, olvídate de la asignación de responsabilidades, reutilización y otras palabras geniales. Veamos el trabajo habitual con tablas:

class ViewController: UIViewController { ... } extension ViewController: UITableViewDelegate { ... } extension ViewController: UITableViewDataSource { ... } 

Analizaremos la opción más común. ¿Qué necesitamos implementar? Correctamente, generalmente se implementan 3 métodos UITableViewDataSource :

 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int func numberOfSections(in tableView: UITableView) -> Int func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) 

Por ahora, no prestaremos atención a los métodos auxiliares ( numberOfSection , etc.) y consideraremos el más interesante: func tableView(tableView: UITableView, indexPath: IndexPath)

Supongamos que queremos completar una tabla con celdas con una descripción de los productos, entonces nuestro método se verá así:

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { let anyCell = tableView.dequeueReusableCell(withIdentifier: ProductCell.self, for: indexPath) guard let cell = anyCell as? ProductCell else { return UITableViewCell() } cell.configure(for: self.products[indexPath.row]) return cell } 

Excelente, no es difícil. Ahora, supongamos que tenemos varios tipos de celdas, por ejemplo, tres:

  • Productos
  • Lista de acciones;
  • Publicidad

Para simplificar el ejemplo, obtenemos la celda para getCell método getCell :

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { switch indexPath.row { case 0: guard let cell: PromoCell = self.getCell() else { return UITableViewCell() } cell.configure(self.promo) return cell case 1: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.ad) return cell default: guard let cell: AdCell = self.getCell() else { return UITableViewCell() } cell.configure(self.products[indexPath.row - 2]) return cell } } 

De alguna manera mucho código. Imagine que queremos hacer la pantalla de configuración. ¿Qué habrá allí?

  • Una tapa de celda con un avatar;
  • Un conjunto de celdas con transiciones "en profundidad";
  • Celdas con interruptores (por ejemplo, habilitar / deshabilitar la entrada por código pin);
  • Celdas con información (por ejemplo, una celda en la que habrá un teléfono, correo electrónico, lo que sea);
  • Ofertas personales

Además, se establece el orden. Un gran método resultará ...

Y ahora otra situación: hay un formulario de entrada. En el formulario de entrada, un grupo de celdas idénticas, cada una de las cuales es responsable de un campo específico en el modelo de datos. Por ejemplo, la celda para ingresar al teléfono es responsable del teléfono, etc.
Todo es simple, pero hay un "PERO". En este caso, aún tiene que pintar diferentes casos, porque necesita actualizar los campos necesarios.

Puede seguir fantaseando e imaginar el diseño impulsado por el backend, en el que recibimos 6 tipos diferentes de campos de entrada, y dependiendo del estado de los campos (visibilidad, tipo de entrada, validación, valor predeterminado, etc.) las celdas cambian tanto que su no puede conducir a una sola interfaz. En este caso, este método se verá muy desagradable. Incluso si descompone la configuración en diferentes métodos.

Por cierto, después de eso, imagine cómo se verá su código si desea agregar / eliminar celdas mientras trabaja. No se verá muy bien porque nos veremos obligados a monitorear de manera independiente la consistencia de los datos almacenados en el ViewController y el número de celdas.

Los problemas:

  • Si hay celdas de diferentes tipos, entonces el código se convierte en un fideo;
  • Hay muchos problemas con el manejo de eventos desde las celdas;
  • Código feo en caso de que necesite cambiar el estado de la tabla.

Problema 2. MindSet


El momento de las palabras geniales aún no ha llegado.
Veamos cómo funciona la aplicación, o más bien, cómo aparecen los datos en la pantalla. Siempre presentamos este proceso secuencialmente. Bueno, más o menos:

  1. Obtener datos de la red;
  2. Para procesar;
  3. Mostrar estos datos en la pantalla.

¿Pero es realmente así? No! De hecho, hacemos esto:

  1. Obtener datos de la red;
  2. Para procesar;
  3. Guardar dentro del modelo ViewController;
  4. Algo provoca una actualización de la pantalla;
  5. El modelo guardado se convierte en celdas;
  6. Los datos se muestran en la pantalla.

Además de la cantidad, todavía hay diferencias. Primero, ya no damos salida a datos; es salida. En segundo lugar, hay un vacío lógico en el proceso de procesamiento de datos, el modelo se guarda y el proceso termina allí. Entonces sucede algo y comienza otro proceso. Por lo tanto, obviamente no agregamos elementos a la pantalla, sino que solo los guardamos (que, por cierto, también está lleno) a pedido.

Y recuerde que UITableViewDelegate , también incluye métodos para determinar la altura de las celdas. Por lo general, automaticDimension suficiente, pero a veces esto no es suficiente y debe establecer la altura usted mismo (por ejemplo, en el caso de animaciones o encabezados)
Luego, generalmente compartimos la configuración de la celda, la parte con la configuración de altura está en otro método.

Los problemas:

  • Se pierde la conexión explícita entre el procesamiento de datos y su visualización en la interfaz de usuario;
  • La configuración de la celda se divide en diferentes partes.

Idea


Los problemas enumerados en las pantallas complejas causan dolor de cabeza y un fuerte deseo de ir a tomar el té.

En primer lugar, no quiero implementar constantemente métodos de delegado. La solución obvia es crear un objeto que lo implemente. A continuación haremos algo como:

 let displayManager = DisplayManager(self.tableView) 

Genial Ahora necesita el objeto para poder trabajar con cualquier celda, mientras que la configuración de estas celdas debe moverse a otro lugar.

Si colocamos la configuración en un objeto separado, entonces encapsulamos (es hora de palabras inteligentes) la configuración en un solo lugar. En este mismo lugar, podemos extraer la lógica para formatear datos (por ejemplo, cambiar el formato de fecha, la concatenación de cadenas, etc.). A través del mismo objeto, podemos suscribirnos a eventos en la celda.

En este caso, tendremos un objeto que tiene dos interfaces diferentes:

  1. La interfaz de generación de instancias de UITableView es para nuestro DisplayManager.
  2. Interfaz de inicialización, suscripción y configuración: para Presenter o ViewController.

Llamamos a este objeto un generador. Entonces, nuestro generador para la tabla es una celda, y para todo lo demás, una forma de presentar datos en una interfaz de usuario y procesar eventos.

Y dado que la configuración ahora está encapsulada por el generador, y el generador en sí es una célula, podemos resolver muchos problemas. Incluidos los enumerados anteriormente.

Implementación


 public protocol TableCellGenerator: class { var identifier: UITableViewCell.Type { get } var cellHeight: CGFloat { get } var estimatedCellHeight: CGFloat? { get } func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell func registerCell(in tableView: UITableView) } public protocol ViewBuilder { associatedtype ViewType: UIView func build(view: ViewType) } 

Con tales implementaciones, podemos hacer la implementación predeterminada:

 public extension TableCellGenerator where Self: ViewBuilder { func generate(tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: self.identifier.nameOfClass, for: indexPath) as? Self.ViewType else { return UITableViewCell() } self.build(view: cell) return cell as? UITableViewCell ?? UITableViewCell() } func registerCell(in tableView: UITableView) { tableView.registerNib(self.identifier) } }<source lang="swift"> 

Daré un ejemplo de un pequeño generador:

 final class FamilyCellGenerator { private var cell: FamilyCell? private var family: Family? var didTapPerson: ((Person) -> Void)? func show(family: Family) { self.family = family cell?.fill(with: family) } func showLoading() { self.family = nil cell?.showLoading() } } extension FamilyCellGenerator: TableCellGenerator { var identifier: UITableViewCell.Type { return FamilyCell.self } } extension FamilyCellGenerator: ViewBuilder { func build(view: FamilyCell) { self.cell = view view.selectionStyle = .none view.didTapPerson = { [weak self] person in self?.didTapPerson?(person) } if let family = self.family { view.fill(with: family) } else { view.showLoading() } } } 

Aquí ocultamos tanto la configuración como las suscripciones. Tenga en cuenta que ahora tenemos un lugar donde podemos encapsular el estado (porque es imposible encapsular el estado en la celda porque la tabla lo reutiliza). Y también tuvieron la oportunidad de cambiar los datos en la celda "sobre la marcha".

Presta atención a self.cell = view . Recordamos la celda y ahora podemos actualizar los datos sin volver a cargar esta celda. Esta es una característica útil.

Pero estaba distraído. Como podemos tener cualquier celda representada por un generador, podemos hacer que la interfaz de nuestro DisplayManager sea un poco más hermosa.

 public protocol DataDisplayManager: class { associatedtype CollectionType associatedtype CellGeneratorType associatedtype HeaderGeneratorType init(collection: CollectionType) func forceRefill() func addSectionHeaderGenerator(_ generator: HeaderGeneratorType) func addCellGenerator(_ generator: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType], after: CellGeneratorType) func addCellGenerator(_ generator: CellGeneratorType, after: CellGeneratorType) func addCellGenerators(_ generators: [CellGeneratorType]) func update(generators: [CellGeneratorType]) func clearHeaderGenerators() func clearCellGenerators() } 

Esto en realidad no es todo. Podemos insertar generadores en los lugares correctos o eliminarlos.

Por cierto, insertar una celda después de una celda específica puede ser muy útil. Especialmente si cargamos gradualmente los datos (por ejemplo, el usuario ingresó el TIN, cargamos la información TIN y la mostramos agregando varias celdas nuevas después del campo TIN).

Resumen


Cómo se verá el trabajo celular ahora:

 class ViewController: UIViewController { func update(data: [Products]) { let gens = data.map { ProductCellGenerator($0) } self.ddm.addGenerators(gens) } } 

O aqui:

 class ViewController: UIViewController { func update(fields: [Field]) { let gens = fields.map { field switch field.type { case .phone: let gen = PhoneCellGenerator(item) gen.didUpdate = { self.updatePhone($0) } return gen case .date: let gen = DateInputCellGenerator(item) gen.didTap = { self.showPicker() } return gen case .dropdown: let gen = DropdownCellGenerator(item) gen.didTap = { self.showDropdown(item) } return gen } } let splitter = SplitterGenerator() self.ddm.addGenerator(splitter) self.ddm.addGenerators(gens) self.ddm.addGenerator(splitter) } } 

Podemos controlar el orden de agregar elementos y, al mismo tiempo, no se pierde la conexión entre el procesamiento de datos y agregarlos a la interfaz de usuario. Por lo tanto, en casos simples, tenemos un código simple. En casos difíciles, el código no se convierte en pasta y al mismo tiempo parece aceptable. Ha aparecido una interfaz declarativa para trabajar con tablas y ahora encapsulamos la configuración de las celdas, lo que en sí mismo nos permite reutilizar las celdas junto con las configuraciones entre diferentes pantallas.

Ventajas de usar RDDM:

  • Encapsulación de la configuración celular;
  • Reducción de la duplicación de código encapsulando el trabajo de las colecciones al adaptador;
  • Seleccione un objeto adaptador que encapsule la lógica específica de trabajar con colecciones;
  • El código se vuelve más obvio y más fácil de leer;
  • Se reduce la cantidad de código que debe escribirse para agregar una tabla;
  • El proceso de procesamiento de eventos desde celdas se simplifica.

Fuentes aquí .

Gracias por su atencion!

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


All Articles