Tabelas genéricas estáticas

imagem

Todos nós geralmente temos que lidar com tabelas estáticas, elas podem ser as configurações de nosso aplicativo, telas de autorização, telas "sobre nós" e muitas outras. Porém, frequentemente, desenvolvedores iniciantes não aplicam nenhum padrão de desenvolvimento a essas tabelas e escrevem em uma classe um sistema não escalável e inflexível.

Sobre como eu resolvo esse problema - sob o corte.

Do que você está falando?


Antes de resolver o problema das tabelas estáticas, você deve entender o que é. Tabelas estáticas são tabelas em que você já sabe o número de linhas e o conteúdo que está nelas. Exemplos de tabelas semelhantes abaixo.

imagem

O problema


Para começar, vale a pena identificar o problema: por que não podemos simplesmente criar um ViewController que será UITableViewDelegate e UITableViewDatasource e apenas descrever todas as células necessárias? Pelo menos - existem 5 problemas com a nossa tabela:

  1. Difícil de escalar
  2. Dependente do índice
  3. Não é flexível
  4. Falta de reutilização
  5. Requer muito código para inicializar

Solução


O método para resolver o problema é baseado no seguinte fundamento:

  1. Remoção da responsabilidade da configuração da tabela em uma classe separada ( Construtor )
  2. Wrapper personalizado sobre UITableViewDelegate e UITableViewDataSource
  3. Conectando células a protocolos personalizados para reutilização
  4. Criando seus próprios modelos de dados para cada tabela

Primeiro, quero mostrar como isso é usado na prática - depois mostrarei como tudo é implementado sob o capô.

Implementação


A tarefa é criar uma tabela com duas células de texto e uma vazia entre elas.

Primeiro de tudo, criei um TextTableViewCell regular com o UILabel .
Em seguida, cada UIViewController com uma tabela estática precisa de seu próprio construtor, vamos criá-lo:

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

Quando o herdamos do StaticConstructorContainer , antes de tudo, o protocolo Generic exige que digitemos o modelo ( ModelType ) - este é o tipo de modelo de célula que também precisamos criar, vamos fazê-lo.

Eu uso enum para isso, pois é mais adequado para nossas tarefas e aqui começa a diversão. Nós preencheremos nossa tabela com conteúdo usando protocolos como: Títulos, Legendas, Cor, Fonte e assim por diante. Como você pode imaginar, esses protocolos são responsáveis ​​pela exibição do texto. Digamos que o protocolo Titled exija título: String? , e se nossa célula suportar a exibição de títulos , ela será preenchida. Vamos ver como é:

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

Consequentemente, apenas uma pequena parte desses protocolos é apresentada aqui; você pode criá-lo, como vê - é muito simples. Lembro que criamos uma vez para uma finalidade e depois as esquecemos e as usamos com calma.

Nossa célula ( com texto ) suporta essencialmente o seguinte: A fonte do texto, o próprio texto, a cor do texto, a cor de fundo da célula e, geralmente, tudo o que vem à mente.

Até agora precisamos apenas do título . Portanto, herdamos nosso modelo de Titled. Dentro do modelo, no caso, indicamos que tipos de células teremos.

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

Como não há rótulo no meio (célula vazia), você pode retornar nulo.
Terminamos a célula C e você pode inseri-la em nosso construtor.

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

E, de fato, esse é todo o nosso código. Podemos dizer que nossa mesa está pronta. Vamos preencher os dados e ver o que acontece.

Ah sim, eu quase esqueci. Precisamos herdar nossa célula do protocolo TitledConfigurable para que ele possa inserir um título em si. As células também suportam alturas dinâmicas.

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

Como é o construtor preenchido:

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

Parece bem compacto, certo?

Na verdade, a última coisa que resta a fazer é conectar tudo ao 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) } } 

Tudo está pronto, temos que tornar delegateDataSource como uma propriedade separada em nossa classe para que o elo mais fraco não se quebre em nenhuma função.

Podemos executar e testar:

imagem

Como você pode ver, tudo funciona.

Agora vamos resumir e entender o que alcançamos:

  1. Se criarmos uma nova célula e desejarmos substituir a atual por ela, faremos isso alterando uma variável. Temos um sistema de mesa muito flexível
  2. Reutilizamos todas as células. Quanto mais células você vincular a esta tabela, mais fácil será trabalhar com ela. Ótimo para grandes projetos.
  3. Reduzimos a quantidade de código para criar a tabela. E teremos que escrever ainda menos quando tivermos muitos protocolos e células estáticas no projeto.
  4. Trouxemos a construção de tabelas estáticas do UIViewController para o Constructor
  5. Paramos de depender dos índices, podemos trocar com segurança as células da matriz e a lógica não será interrompida.

Código para um projeto de teste no final do artigo.

Como funciona de dentro para fora?


Como os protocolos funcionam, já discutimos. Agora precisamos entender como todo o construtor e suas classes associadas funcionam.

Vamos começar com o próprio construtor:
 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 é um protocolo comum que requer funções que já nos são familiares.

Mais interessante é a sua extensão :

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

A função de configuração que chamamos em nosso ViewController registra todas as células para nós e delega dataSource e delega .

E delegateDataSource () cria para nós um invólucro UITableViewDataSource e UITableViewDelegate . Vejamos:

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

Acho que não há perguntas sobre as funções heightForRowAt , numberOfRowsInSection , didSelectRowAt , elas apenas implementam uma funcionalidade clara. O método mais interessante aqui é cellForRowAt .

Nele, não implementamos a lógica mais bonita. Somos forçados a escrever cada novo protocolo nas células aqui, mas fazemos isso uma vez - para que não seja tão assustador. Se o modelo estiver em conformidade com o protocolo, assim como nossa célula, nós o configuraremos. Se alguém tiver idéias sobre como automatizar isso, ficarei feliz em ouvir os comentários.

Isso termina a lógica. Não toquei em classes utilitárias de terceiros neste sistema. Você pode ler o código completo aqui .

Obrigado pela atenção!

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


All Articles