Objeto compuesto "fuente de datos" y elementos de un enfoque funcional

Una vez, (bueno, ni siquiera yo) enfrenté la tarea de agregar una celda de un tipo completamente diferente al UICollectionView con un cierto tipo de celdas, y hacer esto solo en un caso especial, que se procesa "arriba" y no depende directamente de UICollectionView . Esta tarea dio lugar, si mi memoria me sirve, a un par de feos bloques if - else dentro de los UICollectionViewDelegate UICollectionViewDataSource y UICollectionViewDelegate , que se instalaron de forma segura en el código de "producción" y, probablemente, no irán a ningún lado desde allí.

En el marco de la tarea antes mencionada, no tenía sentido pensar en una solución más elegante, o gastar energía "reflexiva" en esto. Sin embargo, recordé esta historia: pensé en tratar de implementar un cierto objeto "fuente de datos", que podría estar compuesto de cualquier número de otros objetos "fuente de datos" en un solo conjunto. La solución, obviamente, debe ser generalizada, adecuada para cualquier número de componentes (incluidos cero y uno) y no depender de tipos específicos. Resultó que esto no solo es real, sino que tampoco es demasiado difícil (aunque hacer que el código también sea "hermoso" es un poco más difícil).

UITableView lo que hice con el ejemplo UITableView . Si lo desea, escribir un código similar para UICollectionView no debería ser difícil.

"Una idea siempre es más importante que su encarnación"


Este aforismo pertenece al gran autor de cómics Alan Moore ( "Keepers" , "V significa Vendetta " , "League of Outstanding Gentlemen" ), pero no se trata realmente de programación, ¿verdad?

La idea principal de mi enfoque es almacenar una matriz de objetos UITableViewDataSource , devolver su número total de secciones y poder determinar al acceder a la sección cuál de los objetos originales "fuente de datos" redirigirá esta llamada.

El protocolo UITableViewDataSource ya tiene los métodos necesarios para obtener el número de secciones, filas, etc., pero, desafortunadamente, en este caso me pareció extremadamente inconveniente usarlo debido a la necesidad de pasar una referencia a una instancia específica de UITableView como uno de los argumentos. Por lo tanto, decidí extender el protocolo estándar UITableViewDataSource con un par de miembros simples adicionales:

 protocol ComposableTableViewDataSource: UITableViewDataSource { var numberOfSections: Int { get } func numberOfRows(for section: Int) -> Int } 

Y el "origen de datos" compuesto resultó ser una clase simple que implementa los requisitos de UITableViewDataSource y se inicializa con un solo argumento: un conjunto de instancias específicas de ComposableTableViewDataSource :

 final class ComposedTableViewDataSource: NSObject, UITableViewDataSource { private let dataSources: [ComposableTableViewDataSource] init(dataSources: ComposableTableViewDataSource...) { self.dataSources = dataSources super.init() } private override init() { fatalError("\(#file) \(#line): Initializer with parameters must be used.") } } 

Ahora solo queda escribir las implementaciones de todos los métodos del protocolo UITableViewDataSource para que se refieran a los métodos de los componentes correspondientes.

“Fue la decisión correcta. Mi decision


Estas palabras pertenecieron a Boris Nikolayevich Yeltsin , el primer presidente de la Federación de Rusia , y en realidad no se refieren al texto a continuación, simplemente me gustaron.

La decisión correcta me pareció utilizar la funcionalidad del lenguaje Swift , y realmente resultó ser conveniente.

Primero, implementamos un método que devuelve el número de secciones; esto no es difícil. Como se mencionó anteriormente, solo necesitamos el número total de todas las secciones de los componentes:

 func numberOfSections(in tableView: UITableView) -> Int { // Default value if not implemented is "1". return dataSources.reduce(0) { $0 + ($1.numberOfSections?(in: tableView) ?? 1) } } 

(No explicaré la sintaxis y el significado de las funciones estándar. Si es necesario, Internet está lleno de buenos artículos introductorios sobre el tema . Y también puedo recomendar un libro bastante bueno ).

Un vistazo rápido a todos los métodos de UITableViewDataSource , notará que como argumentos solo aceptan un enlace a la tabla y el valor del número de sección o la fila correspondiente de IndexPath . Escribiremos algunos ayudantes que nos serán útiles para implementar todos los demás métodos de protocolo.

Primero, todas las tareas pueden reducirse a una función "genérica" , que toma como argumentos una referencia a un ComposableTableViewDataSource específico y el valor del número de sección o IndexPath . Por conveniencia y brevedad, asignaremos alias a los tipos de estas funciones. Además, para mayor legibilidad, sugiero declarar un alias para el número de sección:

 private typealias SectionNumber = Int private typealias AdducedSectionTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ sectionNumber: SectionNumber) -> T private typealias AdducedIndexPathTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ indexPath: IndexPath) -> T 

(Explicaré los nombres seleccionados justo debajo).

En segundo lugar, implementamos una función simple que determina el ComposableTableViewDataSource específico y el número de sección correspondiente por el número de sección ComposedTableViewDataSource :

 private func decompose(section: SectionNumber) -> (dataSource: ComposableTableViewDataSource, decomposedSection: SectionNumber) { var section = section var dataSourceIndex = 0 for (index, dataSource) in dataSources.enumerated() { let diff = section - dataSource.numberOfSections dataSourceIndex = index if diff < 0 { break } else { section = diff } } return (dataSources[dataSourceIndex], section) } 

Quizás, si piensa un poco más que la mía, la implementación resultará más elegante y menos sencilla. Por ejemplo, los colegas inmediatamente sugirieron que implemente una búsqueda binaria en esta función (anteriormente, por ejemplo, durante la inicialización, componiendo el índice del número de secciones, una simple matriz de enteros ). O incluso pase un poco de tiempo compilando y almacenando la tabla de correspondencia de los números de sección, pero luego, en lugar de utilizar el método con la complejidad temporal de O (n) u O (log n), puede obtener el resultado al costo de O (1). Pero decidí seguir el consejo del gran Donald Knuth de no participar en la optimización prematura sin necesidad aparente y medidas apropiadas. Sí, y no sobre este artículo.

Y finalmente, las funciones que aceptan AdducedSectionTask y AdducedIndexPathTask indicadas anteriormente y las "redirigen" a instancias específicas de ComposedTableViewDataSource :

 private func adduce<T>(_ section: SectionNumber, _ task: AdducedSectionTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: section) return task(dataSource, decomposedSection) } private func adduce<T>(_ indexPath: IndexPath, _ task: AdducedIndexPathTask<T>) -> T { let (dataSource, decomposedSection) = decompose(section: indexPath.section) return task(dataSource, IndexPath(row: indexPath.row, section: decomposedSection)) } 

Y ahora puedes explicar los nombres que he elegido para todas estas funciones. Es simple: reflejan un estilo funcional de nombres. Es decir significa literalmente poco, pero suena impresionante.

Las últimas dos funciones parecen casi gemelas, pero después de pensar un poco, dejé de tratar de deshacerme de la duplicación de código, porque traía más inconvenientes que ventajas: tenía que generar o transferir las funciones de conversión al número de sección y volver al tipo original. Además, la probabilidad de reutilizar este enfoque generalizado tiende a cero.

Todos estos preparativos y ayudantes brindan una ventaja increíble en la implementación, de hecho, de los métodos de protocolo. Métodos de configuración de tabla:

 func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForHeaderInSection: $1) } } func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { return adduce(section) { $0.tableView?(tableView, titleForFooterInSection: $1) } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return adduce(section) { $0.tableView(tableView, numberOfRowsInSection: $1) } } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { return adduce(indexPath) { $0.tableView(tableView, cellForRowAt: $1) } } 

Insertar y eliminar filas:

 func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { return adduce(indexPath) { $0.tableView?(tableView, commit: editingStyle, forRowAt: $1) } } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { // Default if not implemented is "true". return adduce(indexPath) { $0.tableView?(tableView, canEditRowAt: $1) ?? true } } 

De manera similar, se puede implementar el soporte para encabezados de índice de sección. En este caso, en lugar del número de sección, debe operar con el índice del encabezado. Además, lo más probable es que sea útil para esto agregar un campo adicional al protocolo ComposableTableViewDataSource . Dejé esta pieza fuera del material.

"Lo imposible hoy será posible mañana"


Estas son las palabras del científico ruso Konstantin Eduardovich Tsiolkovsky , el fundador de la cosmonautica teórica.

En primer lugar, la solución presentada no admite arrastrar y soltar filas. El plan original incluía soporte para arrastrar y soltar dentro de uno de los objetos "fuente de datos" constituyentes, pero, desafortunadamente, esto no se puede lograr usando solo UITableViewDataSource . Los métodos de este protocolo determinan si es posible "arrastrar y soltar" una línea en particular y recibir una "devolución de llamada" al final del arrastre. Y el procesamiento del evento en sí está implícito en los métodos UITableViewDelegate .

En segundo lugar, y lo que es más importante, es necesario pensar en los mecanismos para actualizar los datos en la pantalla. Creo que esto se puede implementar declarando el protocolo del delegado ComposableTableViewDataSource , cuyos métodos serán implementados por ComposedTableViewDataSource y recibir una señal de que el "origen de datos" original ha recibido una actualización. Quedan abiertas dos preguntas: cómo determinar de manera confiable qué ComposableTableViewDataSource ha cambiado dentro de ComposedTableViewDataSource y cómo es una tarea separada y no la más trivial, pero tener una serie de soluciones (por ejemplo, tales ). Y, por supuesto, necesitará el ComposedTableViewDataSource delegado ComposedTableViewDataSource , cuyos métodos se invocarán al actualizar el "origen de datos" compuesto e implementado por el tipo de cliente (por ejemplo, un controlador o modelo de vista ).

Espero investigar mejor estos problemas con el tiempo y cubrirlos en la segunda parte del artículo. Mientras tanto, me divierte, ¡tenías curiosidad por leer sobre estos experimentos!

PS


Justo el otro día, tuve que ingresar al código mencionado en la introducción para su modificación: necesitaba intercambiar las celdas de esos dos tipos. Brevemente, tuve que atormentarme y "profanar" constantemente apareciendo en diferentes lugares. Index out of bounds . Cuando se utiliza el enfoque descrito, solo sería necesario intercambiar dos objetos de "fuente de datos" en la matriz que se pasa como argumento inicializador.

Referencias
- Playgroud con código completo y ejemplo
- mi twitter

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


All Articles