Gerenciador de exibição de dados reativa. 1. Introdução

Esta é a primeira parte de uma série de artigos na biblioteca ReactiveDataDisplayManager (RDDM) . Neste artigo, descreverei os problemas comuns com os quais tenho que lidar ao trabalhar com tabelas "regulares", bem como uma descrição do RDDM.




Problema 1. UITableViewDataSource


Para iniciantes, esqueça a alocação de responsabilidades, a reutilização e outras palavras legais. Vejamos o trabalho usual com tabelas:

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

Analisaremos a opção mais comum. O que precisamos implementar? Corretamente, três métodos UITableViewDataSource geralmente são implementados:

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

Por enquanto, não prestaremos atenção aos métodos auxiliares ( numberOfSection , etc.) e consideraremos o mais interessante - func tableView(tableView: UITableView, indexPath: IndexPath)

Suponha que desejemos preencher uma tabela com células com uma descrição dos produtos; nosso método será semelhante a este:

 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, não é difícil. Agora, suponha que tenhamos vários tipos de células, por exemplo, três:

  • Produtos>
  • Lista de ações;
  • Publicidade.

Para simplificar o exemplo, obtemos o método getCell da célula:

 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 alguma forma, muito código. Imagine que queremos criar a tela de configurações. O que vai estar aí?

  • Uma tampa de celular com um avatar;
  • Um conjunto de células com transições "em profundidade";
  • Células com comutadores (por exemplo, ativar / desativar a entrada por código PIN);
  • Células com informações (por exemplo, uma célula na qual haverá um telefone, email, qualquer que seja);
  • Ofertas pessoais.

Além disso, o pedido está definido. Um ótimo método será o ...

E agora outra situação - há um formulário de entrada. No formulário de entrada, um monte de células idênticas, cada uma das quais é responsável por um campo específico no modelo de dados. Por exemplo, o celular para entrar no telefone é responsável pelo telefone e assim por diante.
Tudo é simples, mas existe um "MAS". Nesse caso, você ainda precisa pintar casos diferentes, porque é necessário atualizar os campos necessários.

Você pode continuar fantasiando e imaginando o design orientado para o back-end, no qual recebemos 6 tipos diferentes de campos de entrada e, dependendo do estado dos campos (visibilidade, tipo de entrada, validação, valor padrão etc.), as células mudam tanto que suas células não pode levar a uma única interface. Nesse caso, esse método parecerá muito desagradável. Mesmo se você decompor a configuração em métodos diferentes.

A propósito, depois disso, imagine como será seu código se você quiser adicionar / remover células enquanto trabalha. Não parecerá muito bom porque seremos forçados a monitorar independentemente a consistência dos dados armazenados no ViewController e o número de células.

Os problemas:

  • Se houver células de tipos diferentes, o código se tornará macarrão;
  • Existem muitos problemas ao lidar com eventos de células;
  • Código feio, caso você precise alterar o estado da tabela.

Problema 2. MindSet


O tempo para palavras legais ainda não chegou.
Vejamos como o aplicativo funciona, ou melhor, como os dados aparecem na tela. Sempre apresentamos esse processo sequencialmente. Bem, mais ou menos:

  1. Obter dados da rede;
  2. Processar;
  3. Exiba esses dados na tela.

Mas é mesmo assim? Não! De fato, fazemos o seguinte:

  1. Obter dados da rede;
  2. Processar;
  3. Salve dentro do modelo ViewController;
  4. Algo causa uma atualização da tela;
  5. O modelo salvo é convertido em células;
  6. Os dados são exibidos na tela.

Além da quantidade, ainda existem diferenças. Primeiro, não fornecemos mais dados; eles são gerados. Em segundo lugar, existe uma lacuna lógica no processo de processamento de dados, o modelo é salvo e o processo termina aí. Então, algo acontece e outro processo é iniciado. Portanto, obviamente, não adicionamos elementos à tela, mas apenas os salvamos (que, a propósito, também são pesados) sob demanda.

E lembre-se sobre o UITableViewDelegate , ele também inclui métodos para determinar a altura das células. Normalmente, automaticDimension suficiente, mas às vezes isso não é suficiente e você precisa definir a altura (por exemplo, no caso de animações ou cabeçalhos)
Em seguida, geralmente compartilhamos as configurações da célula, a parte com a configuração da altura está em outro método.

Os problemas:

  • A conexão explícita entre o processamento de dados e sua exibição na interface do usuário é perdida;
  • A configuração da célula é dividida em partes diferentes.

Idéia


Os problemas listados em telas complexas causam dor de cabeça e um forte desejo de tomar chá.

Primeiro, não quero implementar métodos delegados constantemente. A solução óbvia é criar um objeto que o implementará. Em seguida, faremos algo como:

 let displayManager = DisplayManager(self.tableView) 

Ótimo. Agora você precisa que o objeto possa trabalhar com qualquer célula, enquanto a configuração dessas células precisa ser movida para outro lugar.

Se colocarmos a configuração em um objeto separado, encapsularemos (é hora de palavras inteligentes) a configuração em um só lugar. Nesse mesmo local, podemos extrair a lógica para formatar dados (por exemplo, alterar o formato da data, concatenação de cadeias, etc.). Através do mesmo objeto, podemos assinar eventos na célula.

Nesse caso, teremos um objeto que possui duas interfaces diferentes:

  1. A interface de geração de instância UITableView é para o nosso DisplayManager.
  2. Interface de inicialização, assinatura e configuração - para Presenter ou ViewController.

Chamamos esse objeto de gerador. Então, nosso gerador para a tabela é uma célula e para todo o resto - uma maneira de apresentar dados em uma interface do usuário e processar eventos.

E como a configuração agora está encapsulada pelo gerador, e o próprio gerador é uma célula, podemos resolver muitos problemas. Incluindo os listados acima.

Implementação


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

Com essas implementações, podemos fazer a implementação padrão:

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

Vou dar um exemplo de um pequeno gerador:

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

Aqui, ocultamos a configuração e as assinaturas. Observe que agora temos um local onde podemos encapsular o estado (porque é impossível encapsular o estado na célula porque ele é reutilizado pela tabela). E eles também tiveram a oportunidade de alterar os dados na célula "on the fly".

Preste atenção em self.cell = view . Lembramos a célula e agora podemos atualizar os dados sem recarregar essa célula. Esse é um recurso útil.

Mas eu estava distraído. Como podemos ter qualquer célula representada por um gerador, podemos tornar a interface do nosso DisplayManager um pouco mais bonita.

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

Na verdade, isso não é tudo. Podemos inserir geradores nos lugares certos ou excluí-los.

A propósito, inserir uma célula após uma célula específica pode ser extremamente útil. Especialmente se carregarmos gradualmente os dados (por exemplo, o usuário inseriu o TIN, carregamos as informações do TIN e as exibimos adicionando várias novas células após o campo TIN).

Sumário


Como será o trabalho da célula:

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

Ou 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 a ordem de adicionar elementos e, ao mesmo tempo, a conexão entre o processamento de dados e a adição deles à interface do usuário não é perdida. Assim, em casos simples, temos código simples. Em casos difíceis, o código não se transforma em massa e, ao mesmo tempo, parece aceitável. Uma interface declarativa para trabalhar com tabelas apareceu e agora encapsulamos a configuração de células, o que por si só nos permite reutilizar células juntamente com configurações entre diferentes telas.

Profissionais do uso do RDDM:

  • Encapsulamento da configuração celular;
  • Reduzindo a duplicação de código, encapsulando o trabalho das coleções para o adaptador;
  • Selecione um objeto do adaptador que encapsule a lógica específica de trabalhar com coleções;
  • O código se torna mais óbvio e mais fácil de ler;
  • A quantidade de código que precisa ser gravada para adicionar uma tabela é reduzida;
  • O processo de processamento de eventos a partir das células é simplificado.

Fontes aqui .

Obrigado pela atenção!

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


All Articles