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:
- Obter dados da rede;
- Processar;
- Exiba esses dados na tela.
Mas é mesmo assim? Não! De fato, fazemos o seguinte:
- Obter dados da rede;
- Processar;
- Salve dentro do modelo ViewController;
- Algo causa uma atualização da tela;
- O modelo salvo é convertido em células;
- 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:
- A interface de geração de instância
UITableView
é para o nosso DisplayManager. - 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!