Uma vez, eu (bem, nem eu) enfrentou a tarefa de adicionar uma célula de um tipo completamente diferente ao
UICollectionView
com um determinado tipo de célula, e fazer isso apenas em um caso especial, que é processado "acima" e não depende diretamente do
UICollectionView
. Essa tarefa deu origem, se minha memória me servir, a alguns feios blocos
if
- else
dentro dos
UICollectionViewDelegate
e
UICollectionViewDelegate
, que se estabeleceram com segurança no código de "produção" e, provavelmente, não irão a lugar nenhum.
Dentro da estrutura da tarefa acima mencionada, não havia sentido em pensar em uma solução mais elegante ou em desperdiçar energia "pensativa" nisso. No entanto, lembrei-me dessa história: eu estava pensando em tentar implementar um certo objeto de "fonte de dados" que pudesse ser composto por qualquer número de outros objetos de "fonte de dados" em um único todo. A solução, obviamente, deve ser generalizada, adequada para qualquer número de componentes (incluindo zero e um) e não depende de tipos específicos. Verificou-se que isso não é apenas real, mas também não é muito difícil (embora tornar o código também "bonito" seja um pouco mais difícil).
Vou mostrar o que fiz com o exemplo
UITableView
. Se desejar, escrever um código semelhante para o
UICollectionView
não deve ser difícil.
“Uma ideia é sempre mais importante que sua personificação”
Esse aforismo pertence ao grande autor de quadrinhos Alan Moore ( "Keepers" , "V significa Vendetta " , "League of Outstanding Gentlemen" ), mas não é realmente sobre programação, certo?A idéia principal da minha abordagem é armazenar uma matriz de objetos
UITableViewDataSource
, retornar o número total de seções e poder determinar ao acessar a seção quais objetos originais da "fonte de dados" redirecionarão essa chamada.
O protocolo
UITableViewDataSource
já possui os métodos necessários para obter o número de seções, linhas, etc., mas, infelizmente, neste caso, achei extremamente inconveniente usar devido à necessidade de passar uma referência a uma instância específica do
UITableView
como um dos argumentos. Portanto, decidi estender o protocolo
UITableViewDataSource
padrão com alguns membros simples adicionais:
protocol ComposableTableViewDataSource: UITableViewDataSource { var numberOfSections: Int { get } func numberOfRows(for section: Int) -> Int }
E a “fonte de dados” composta acabou sendo uma classe simples que implementa os requisitos de
UITableViewDataSource
e é inicializada com apenas um argumento - um conjunto de instâncias 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.") } }
Agora, resta apenas gravar implementações de todos os métodos do protocolo
UITableViewDataSource
para que eles se refiram aos métodos dos componentes correspondentes.
“Foi a decisão certa. Minha decisão
Essas palavras pertenciam a Boris Nikolayevich Yeltsin , o primeiro presidente da Federação Russa , e não se referem realmente ao texto abaixo, apenas gostei delas.A decisão certa me pareceu usar a
funcionalidade da linguagem
Swift , e realmente se mostrou conveniente.
Primeiro, implementamos um método que retorna o número de seções - isso não é difícil. Como mencionado acima, precisamos apenas do número total de todas as seções dos componentes:
func numberOfSections(in tableView: UITableView) -> Int {
(Não explicarei a sintaxe e o significado das funções padrão. Se necessário, a Internet está cheia de
bons artigos introdutórios sobre o assunto . E também posso recomendar um
livro muito bom .)
Uma rápida olhada em todos os métodos de
UITableViewDataSource
, você observará que, como argumentos, eles aceitam apenas um link para a tabela e o valor do número da seção ou da linha
IndexPath
correspondente. Escreveremos alguns auxiliares que nos serão úteis na implementação de todos os outros métodos de protocolo.
Primeiro, todas as tarefas podem ser reduzidas a uma função
"genérica" , que usa como argumento uma referência a um
ComposableTableViewDataSource
específico e o valor do número da seção ou
IndexPath
. Por conveniência e brevidade, atribuímos
pseudônimos aos tipos dessas funções. Além disso, para maior legibilidade, sugiro declarar um alias para o número da seção:
private typealias SectionNumber = Int private typealias AdducedSectionTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ sectionNumber: SectionNumber) -> T private typealias AdducedIndexPathTask<T> = (_ composableDataSource: ComposableTableViewDataSource, _ indexPath: IndexPath) -> T
(Vou explicar os nomes selecionados logo abaixo.)
Em segundo lugar, implementamos uma função simples que determina o
ComposableTableViewDataSource
específico e o número da seção correspondente pelo número da seção
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) }
Talvez, se você pensar um pouco mais do que o meu, a implementação se mostrará mais elegante e menos direta. Por exemplo, colegas imediatamente sugeriram que eu implementasse uma
pesquisa binária nessa função (anteriormente, por exemplo, durante a inicialização, compondo o índice do número de seções - uma
matriz simples de
números inteiros ). Ou até gaste um pouco de tempo compilando e armazenando a tabela de correspondência dos números de seção, mas, em vez de usar o método com
complexidade de tempo de O (n) ou O (log n), você pode obter o resultado ao custo de O (1). Mas eu decidi seguir o conselho do grande
Donald Knuth para não se envolver em otimização prematura sem necessidade aparente e medidas apropriadas. Sim, e não sobre este artigo.
E, finalmente, funções que aceitam as
AdducedSectionTask
e
AdducedIndexPathTask
indicadas acima e as "redirecionam" para instâncias 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)) }
E agora você pode explicar os nomes que escolhi para todas essas funções. É simples: eles refletem um estilo de nomeação funcional. I.e. significa literalmente pouco, mas soa impressionante.
As duas últimas funções parecem quase gêmeos, mas, depois de um pouco de reflexão, desisti de tentar me livrar da duplicação de código, porque trazia mais inconveniência do que vantagens: eu precisava produzir ou transferir as funções de conversão para o número da seção e voltar ao tipo original. Além disso, a probabilidade de reutilizar essa abordagem generalizada tende a zero.
Todos esses preparativos e auxiliares oferecem uma vantagem incrível na implementação, de fato, dos métodos de protocolo. Métodos de configuração da tabela:
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) } }
Inserir e excluir linhas:
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 maneira semelhante, o suporte para cabeçalhos de índice de seção pode ser implementado. Nesse caso, em vez do número da seção, você deve operar com o índice do cabeçalho. Além disso, provavelmente, será útil adicionar um campo adicional ao protocolo
ComposableTableViewDataSource
. Deixei esta peça fora do material.
"O impossível hoje será possível amanhã"
Estas são as palavras do cientista russo Konstantin Eduardovich Tsiolkovsky , o fundador da cosmonáutica teórica.Em primeiro lugar, a solução apresentada não suporta arrastar e soltar linhas. O plano original incluía suporte para arrastar e soltar dentro de um dos objetos "fonte de dados" constituinte, mas, infelizmente, isso não pode ser alcançado usando apenas
UITableViewDataSource
. Os métodos deste protocolo determinam se é possível “arrastar e soltar” uma linha específica e receber um “retorno de chamada” no final do arrasto. E o processamento do evento em si está implícito nos métodos
UITableViewDelegate
.
Em segundo lugar, e mais importante, é necessário pensar em mecanismos para atualizar dados na tela. Acho que isso pode ser implementado declarando o protocolo de delegação
ComposableTableViewDataSource
, cujos métodos serão implementados por
ComposedTableViewDataSource
e recebendo um sinal de que a "fonte de dados" original recebeu uma atualização. Duas perguntas permanecem em aberto: como determinar com segurança qual
ComposableTableViewDataSource
foi alterada dentro do
ComposedTableViewDataSource
e como é uma tarefa separada e não a mais trivial, mas com várias soluções (por exemplo,
tais ). E, é claro, você precisará do
ComposedTableViewDataSource
delegação
ComposedTableViewDataSource
, cujos métodos serão chamados ao atualizar a "fonte de dados" composta e implementada pelo tipo de cliente (por exemplo, um
controlador ou
modelo de exibição ).
Espero investigar melhor esses problemas ao longo do tempo e abordá-los na segunda parte do artigo. Enquanto isso, me diverte, você estava curioso para ler sobre esses experimentos!
PS
Outro dia, tive que entrar no código mencionado na introdução para modificá-lo: precisava trocar as células desses dois tipos. Em resumo, tive que me atormentar e "ganhar a vida" com o
Index out of bounds
constantemente aparecendo em lugares diferentes. Ao usar a abordagem descrita, seria necessário trocar apenas dois objetos "fonte de dados" na matriz passados como um argumento inicializador.
Referências:-
Playgroud com código e exemplo completos-
meu twitter