Primeiros passos com UITableViewDiffableDataSource — Parte 1
Quem já usou UITableView
antes do iOS 12 e nunca "crashou" o aplicativo que atire a primeira pedra. É muito fácil remover ou adicionar um elemento e se esquecer de manter tudo sincronizado entre atableView
e seu dataSource
.
Qual era o problema?
A UITableView
possui uma propriedade chamada dataSource
, que recebe um tipo que deve conformar ao protocolo UITableViewDataSource
. O dataSource
é responsável por fornecer à tableView
informações como a quantidade de linhas por seção, quantidade de seções, bem como a célula que será usada para a tableView
exibir. Sendo assim, independentemente de onde o dataSource
for buscar as informações, ele é o responsável por fornecer elas para a tableView
. Os métodos obrigatórios do UITableViewDataSource
são:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Intfunc tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
Ao fazer um update visual na tableView
removendo ou adicionando uma linha, por exemplo, precisamos garantir que o dataSource
esteja sincronizado com a tableView
, removendo esse elemento em específico também. Caso isso não ocorra, teremos uma inconsistência, ou até um crash em nosso aplicativo.
tableView.deleteRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
// Se o dataSource não buscar os dados atualizados, refletindo essa remoção, temos um crash :(
A mensagem do crash é parecida com a mensagem abaixo. No meu caso eu tinha uma seção e três linhas, e removi a primeira linha sem atualizar o array dos elementos consultado pelo dataSource
.
Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (3) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).’
A solução
Na WWDC 2019, a Apple apresentou uma nova API para deixar as coisas mais simples e seguras, tanto para aUITableView
quanto para a UICollectionView
. Através dessa API, conseguimos fazer updates e animações de forma mais simples e com menos chance de crash. Como em nosso projeto estamos utilizando UITableView
, vamos utilizar a UITableViewDiffableDataSource
.
Ao invés de criar um projeto direto com o UITableViewDiffableDataSource
, pensei ser mais didático criar um projeto que utiliza o UITableViewDataSource
para, em seguida, fazer as alterações necessárias para utilizar essa nova API.
Se quiser entender o projeto que eu criei e a proposta, só vir aqui.
Se quiser pular essa pequena leitura, você pode baixar o projeto inicial desse estudo logo abaixo, caso queira ir fazendo junto comigo.
UITableViewDiffableDataSource
A primeira coisa que precisamos fazer é justamente trocar o nosso dataSource
.
Abra o arquivo DiaryViewController
e apague tanto a atribuição do dataSource
…
tableView.dataSource = self
…quanto a implementação do protocolo.
//Apague a extensão e todo o seu conteúdo
extension DiaryViewController: UITableViewDataSource {
}
Agora vamos ver a assinatura do nosso novo dataSource
e do init dele:
open class UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> : NSObject, UITableViewDataSource where SectionIdentifierType : Hashable, ItemIdentifierType : Hashablepublic typealias CellProvider = (UITableView, IndexPath, ItemIdentifierType) -> UITableViewCell?public init(tableView: UITableView, cellProvider: @escaping UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider)
Note que ele é um tipo genérico, onde SectionIdentifierType
e o ItemIdentifierType
precisam ser do tipo Hashable
. A necessidade de ser Hashable
é porque tanto as seções quanto os elementos precisam ser únicos.
O init
dele recebe uma tableView
e um cellProvider
, que retorna uma célula (não nos lembra ocellForRow
?)
Antes de implementarmos nosso dataSource
, precisamos de um tipo
para a seção, que conforme a Hashable
e o nosso modelo DiaryNote
precisa conformar também. Para nosso aplicativo, criei um enum
apenas com o caso main
para a seção.
Pronto, agora podemos implementar nosso dataSource
.
NSDiffableDataSourceSnapshot
Se você tentar executar o projeto, vai ver que ainda não está funcionando. A tableView
ainda está vazia, certo? Isso é porque o dataSource
faz updates na tableView
através do NSDiffableDataSourceSnapshot
.
A única coisa que precisamos fazer é aplicar um snapshot
no dataSource
. O dataSource
então verifica o estado do último snapshot
e faz um diff com o snapshot
atual. Com esse diff ele consegue fazer todo o update na tableView
.
Simples, não?
Como não temos um snapshot
inicial, a tableView
permanece vazia. Para criar um snapshot
, precisamos dos seguintes passos em nosso projeto:
- Inicializar o
NSDiffableDataSourceSnapshot
, que é do tipo genérico, igual aoUITableViewDiffableDataSource
. - Adicionar a seção
main
que temos, e adicionar as notas nessa seção. - Em seguida, precisamos aplicar o
snapshot
em nossodataSource
.
Se você está se perguntando porque passamos o animatingDifferences
como false
, você realmente é um leitor bem atento! Esse parâmetro indica que não precisamos animar a tableView
nesse snapshot
.
Isso é bem interessante, pois realmente não precisamos animar na primeira iteração. Já queremos visualizar a tela com os elementos iniciais carregados.
Pronto! Já podemos executar nosso aplicativo, e tudo parece estar funcionando normalmente…
Ops, não está :(
Alguma coisa não está certa. Ao clicar no botão edit
, vemos que não conseguimos apagar e nem mover nossas células.
E pior ainda. Ao tentar criar uma nota nova, ocorre um crash em nosso aplicativo.
Na parte 2 do nosso estudo, eu ensino como corrigir isso! :)
Confira a parte 2 clicando aqui!
Até lá!