Tablas Genéricas Estáticas

imagen

Todos a menudo tenemos que lidiar con tablas estáticas, pueden ser la configuración de nuestra aplicación, pantallas de autorización, pantallas "sobre nosotros" y muchas otras. Pero a menudo, los desarrolladores novatos no aplican ningún patrón de desarrollo para tales tablas y escriben todos en una clase un sistema inflexible y no escalable.

Sobre cómo resuelvo este problema, debajo del corte.

De que estas hablando


Antes de resolver el problema de las tablas estáticas, debe comprender qué es. Las tablas estáticas son tablas donde ya conoce el número de filas y el contenido que hay en ellas. Ejemplos de tablas similares a continuación.

imagen

El problema


Para comenzar, vale la pena identificar el problema: ¿por qué no podemos crear un ViewController que será UITableViewDelegate y UITableViewDatasource y solo describir todas las celdas que necesita? Al menos, hay 5 problemas con nuestra tabla:

  1. Difícil de escalar
  2. Índice dependiente
  3. No flexible
  4. Falta de reutilización
  5. Requiere mucho código para inicializar

Solución


El método para resolver el problema se basa en la siguiente base:

  1. Eliminación de la responsabilidad de la configuración de la tabla en una clase separada ( Constructor )
  2. Contenedor personalizado sobre UITableViewDelegate y UITableViewDataSource
  3. Conexión de celdas a protocolos personalizados para su reutilización
  4. Crear sus propios modelos de datos para cada tabla

Primero quiero mostrar cómo se usa esto en la práctica, luego mostraré cómo se implementa todo bajo el capó.

Implementación


La tarea es crear una tabla con dos celdas de texto y una vacía entre ellas.

En primer lugar, creé un TextTableViewCell normal con UILabel .
A continuación, cada UIViewController con una tabla estática necesita su propio Constructor, creémoslo:

class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = <#type#> } 

Cuando lo heredamos del StaticConstructorContainer , en primer lugar, el protocolo genérico requiere que escribamos el modelo ( ModelType ); este es el tipo de modelo de celda que también necesitamos crear, hagámoslo.

Utilizo enum para esto, ya que es más adecuado para nuestras tareas y aquí comienza la diversión. Completaremos nuestra tabla con contenido utilizando protocolos como: Titulado, Subtitulado, Coloreado, Con fuente, etc. Como puede adivinar, estos protocolos son responsables de mostrar el texto. Digamos que el protocolo Titulado requiere título: ¿Cadena? , y si nuestra celda admite pantallas de título , la llenará. Veamos cómo se ve:

 protocol Fonted { var font: UIFont? { get } } protocol FontedConfigurable { func configure(by model: Fonted) } protocol Titled { var title: String? { get } } protocol TitledConfigurable { func configure(by model: Titled) } protocol Subtitled { var subtitle: String? { get } } protocol SubtitledConfigurable { func configure(by model: Subtitled) } protocol Imaged { var image: UIImage? { get } } protocol ImagedConfigurable { func configure(by model: Imaged) } 

En consecuencia, aquí solo se presenta una pequeña parte de dichos protocolos, como puede ver, puede crearlo usted mismo, es muy simple. Les recuerdo que los creamos 1 vez para 1 propósito y luego los olvidamos y los usamos con calma.

Nuestra celda ( con texto ) admite esencialmente las siguientes cosas: la fuente del texto, el texto en sí, el color del texto, el color de fondo de la celda y, en general, cualquier cosa que se le ocurra.

Solo necesitamos un título hasta ahora. Por lo tanto, heredamos nuestro modelo de Titled. Dentro del modelo en el caso, indicamos qué tipos de células tendremos.

 enum CellModel: Titled { case firstText case emptyMiddle case secondText var title: String? { switch self { case .firstText: return " - " case .secondText: return " - " default: return nil } } } 

Como no hay una etiqueta en el medio (celda vacía), puede devolver nil.
Terminamos la celda C y puedes insertarla en nuestro constructor.

 class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = CellModel var models: [CellModel] //        ,    func cellType(for model: CellModel) -> Self.StaticTableViewCellClass.Type { //      ,    } func configure(cell: UITableViewCell, by model: CellModel) { //      ,   ,      } func itemSelected(item: CellModel) { //  didSelect,     } } 

Y de hecho, este es todo nuestro código. Podemos decir que nuestra mesa está lista. Completemos los datos y veamos qué sucede.

Oh sí, casi lo olvido. Necesitamos heredar nuestra celda del protocolo TitledConfigurable para que pueda insertar un título en sí mismo. Las células también soportan alturas dinámicas.

 extension TextTableViewCell: TitledConfigurable { func configure(by model: Titled) { label.text = model.title } } 

Aspecto del constructor relleno:

 class ViewControllerConstructor: StaticConstructorContainer { typealias ModelType = CellModel var models: [CellModel] = [.firstText, .emptyMiddle, .secondText] func cellType(for model: CellModel) -> StaticTableViewCellClass.Type { switch model { case .emptyMiddle: return EmptyTableViewCell.self case .firstText, .secondText: return TextTableViewCell.self } } func configure(cell: UITableViewCell, by model: CellModel) { cell.selectionStyle = .none } func itemSelected(item: CellModel) { switch item { case .emptyMiddle: print("  ") default: print("  ...") } } } 

Se ve bastante compacto, ¿verdad?

En realidad, lo último que nos queda por hacer es conectarlo todo al ViewController'e:

 class ViewController: UIViewController { private let tableView: UITableView = { let tableView = UITableView() return tableView }() private let constructor = ViewControllerConstructor() private lazy var delegateDataSource = constructor.delegateDataSource() override func viewDidLoad() { super.viewDidLoad() constructor.setup(at: tableView, dataSource: delegateDataSource) } } 

Todo está listo, tenemos que hacer delegateDataSource como una propiedad separada en nuestra clase para que el enlace débil no se rompa dentro de ninguna función.

Podemos correr y probar:

imagen

Como puedes ver, todo funciona.

Ahora resumamos y comprendamos lo que hemos logrado:

  1. Si creamos una nueva celda y queremos reemplazar la actual con ella, entonces lo hacemos cambiando una variable. Tenemos un sistema de mesas muy flexible.
  2. Reutilizamos todas las células. Cuantas más celdas enlace a esta tabla, más fácil y más fácil será trabajar con ella. Ideal para grandes proyectos.
  3. Hemos reducido la cantidad de código para crear la tabla. Y tendremos que escribirlo aún menos cuando tengamos muchos protocolos y celdas estáticas en el proyecto.
  4. Llevamos la construcción de tablas estáticas del UIViewController al Constructor
  5. Hemos dejado de depender de los índices, podemos intercambiar de forma segura las celdas en la matriz y la lógica no se romperá.

Código para un proyecto de prueba al final del artículo.

¿Cómo funciona de adentro hacia afuera?


Cómo funcionan los protocolos que ya hemos discutido. Ahora necesitamos entender cómo funciona todo el constructor y sus clases asociadas.

Comencemos con el propio constructor:
 protocol StaticConstructorContainer { associatedtype ModelType var models: [ModelType] { get } func cellType(for model: ModelType) -> StaticTableViewCellClass.Type func configure(cell: UITableViewCell, by model: ModelType) func itemSelected(item: ModelType) } 

Este es un protocolo común que requiere características que ya conocemos.

Más interesante es su extensión :

 extension StaticConstructorContainer { typealias StaticTableViewCellClass = StaticCell & NibLoadable func delegateDataSource() -> StaticDataSourceDelegate<Self> { return StaticDataSourceDelegate<Self>.init(container: self) } func setup<T: StaticConstructorContainer>(at tableView: UITableView, dataSource: StaticDataSourceDelegate<T>) { models.forEach { (model) in let type = cellType(for: model) tableView.register(type.nib, forCellReuseIdentifier: type.name) } tableView.delegate = dataSource tableView.dataSource = dataSource dataSource.tableView = tableView } } 

La función de configuración que llamamos en nuestro ViewController registra todas las celdas para nosotros y delega dataSource y delegate .

Y delegateDataSource () crea para nosotros un contenedor UITableViewDataSource y UITableViewDelegate . Veámoslo:

 class StaticDataSourceDelegate<Container: StaticConstructorContainer>: NSObject, UITableViewDelegate, UITableViewDataSource { private let container: Container weak var tableView: UITableView? init(container: Container) { self.container = container } func reload() { tableView?.reloadData() } func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { let type = container.cellType(for: container.models[indexPath.row]) return type.estimatedHeight ?? type.height } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let type = container.cellType(for: container.models[indexPath.row]) return type.height } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return container.models.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let model = container.models[indexPath.row] let type = container.cellType(for: model) let cell = tableView.dequeueReusableCell(withIdentifier: type.name, for: indexPath) if let typedCell = cell as? TitledConfigurable, let titled = model as? Titled { typedCell.configure(by: titled) } if let typedCell = cell as? SubtitledConfigurable, let subtitle = model as? Subtitled { typedCell.configure(by: subtitle) } if let typedCell = cell as? ImagedConfigurable, let imaged = model as? Imaged { typedCell.configure(by: imaged) } container.configure(cell: cell, by: model) return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let model = container.models[indexPath.row] container.itemSelected(item: model) } } 

Creo que no hay preguntas sobre las funciones heightForRowAt , numberOfRowsInSection , didSelectRowAt , solo implementan una funcionalidad clara. El método más interesante aquí es cellForRowAt .

En él, no implementamos la lógica más bella. Nos vemos obligados a escribir cada protocolo nuevo en las celdas aquí, pero lo hacemos una vez, por lo que no da tanto miedo. Si el modelo cumple con el protocolo, al igual que nuestra célula, lo configuraremos. Si alguien tiene ideas sobre cómo automatizar esto, estaré encantado de escuchar en los comentarios.

Esto termina la lógica. No toqué clases utilitarias de terceros en este sistema, puedes leer el código completo aquí .

Gracias por su atencion!

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


All Articles