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 {
(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 {
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