Primeiros passos com UITableViewDiffableDataSource — Parte 1

Roberto Sampaio
My iOS Studies [pt-br]
4 min readApr 10, 2021

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.

Photo by Annie Spratt on Unsplash

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 à tableViewinformaçõ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 UITableViewDataSourcesã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 DiaryViewControllere 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 dataSourcee 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 ItemIdentifierTypeprecisam 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 tableViewainda está vazia, certo? Isso é porque o dataSource faz updates na tableViewatravé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:

  1. Inicializar o NSDiffableDataSourceSnapshot, que é do tipo genérico, igual ao UITableViewDiffableDataSource.
  2. Adicionar a seção main que temos, e adicionar as notas nessa seção.
  3. Em seguida, precisamos aplicar o snapshot em nosso dataSource.

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!

Link do projeto inicial.

Link do projeto final.

Até lá!

--

--

Roberto Sampaio
My iOS Studies [pt-br]

Servo de Jesus, aprendendo a escrever, desenvolver software, lutar e tocar guitarra. Aprendendo, sempre.