Para que serve NSDiffableDataSourceSnapshot `reloadItems`?
Estou tendo dificuldade em encontrar o uso de NSDiffableDataSourceSnapshot reloadItems(_:):
Se o item que peço para recarregar não for igual a um item que já está presente na fonte de dados, eu travo.
Mas se o item for equiparável a um item que já está presente na fonte de dados, então de que adianta "recarregá-lo"?
Você pode pensar que a resposta ao segundo ponto é: bem, pode haver algum outro aspecto do objeto identificador de item que não faz parte de sua equalização, mas reflete na interface da célula. Mas o que descobri é que isso não é verdade; após a chamada reloadItems, a visualização da tabela não reflete a mudança.
Então, quando eu quero mudar um item, o que acabo fazendo com o instantâneo é um insertdepois do item a ser substituído e depois um deletedo item original. Não existe um replacemétodo de instantâneo , que é o que eu esperava reloadItemsque fosse.
(Eu fiz uma pesquisa Stack Overflow nesses termos e encontrei muito pouco - a maioria apenas algumas perguntas que intrigavam sobre usos específicos de reloadItems, como Como atualizar uma célula de tabela usando UITableView diffable . Portanto, estou perguntando de uma forma mais generalizada , que uso prático alguém encontrou para este método?)
Bem, não há nada como ter um exemplo reproduzível mínimo para brincar, então aqui está um.
Faça um projeto simples do iOS com seu modelo ViewController e adicione este código ao ViewController.
Vou pegar pedaço por pedaço. Primeiro, temos uma estrutura que servirá como nosso identificador de item. O UUID é a parte única, portanto a equatabilidade e a capacidade de hash dependem apenas dele:
struct UniBool : Hashable {
let uuid : UUID
var bool : Bool
// equatability and hashability agree, only the UUID matters
func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
static func ==(lhs:Self, rhs:Self) -> Bool {
lhs.uuid == rhs.uuid
}
}
Em seguida, a visualização de tabela (falsa) e a fonte de dados difundível:
let tableView = UITableView(frame: .zero, style: .plain)
var datasource : UITableViewDiffableDataSource<String,UniBool>!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
self.datasource = UITableViewDiffableDataSource<String,UniBool>(tableView: self.tableView) { tv, ip, isOn in
let cell = tv.dequeueReusableCell(withIdentifier: "cell", for: ip)
return cell
}
var snap = NSDiffableDataSourceSnapshot<String,UniBool>()
snap.appendSections(["Dummy"])
snap.appendItems([UniBool(uuid: UUID(), bool: true)])
self.datasource.apply(snap, animatingDifferences: false)
}
Portanto, há apenas um UniBool em nossa fonte de dados diferenciável e boolé true. Portanto, agora configure um botão para chamar este método de ação que tenta alternar o boolvalor usando reloadItems:
@IBAction func testReload() {
if let unibool = self.datasource.itemIdentifier(for: IndexPath(row: 0, section: 0)) {
var snap = self.datasource.snapshot()
var unibool = unibool
unibool.bool = !unibool.bool
snap.reloadItems([unibool]) // this is the key line I'm trying to test!
print("this object's isOn is", unibool.bool)
print("but looking right at the snapshot, isOn is", snap.itemIdentifiers[0].bool)
delay(0.3) {
self.datasource.apply(snap, animatingDifferences: false)
}
}
}
Então aqui está a coisa. Eu disse para reloadItemscom um item cujo UUID é uma correspondência, mas cujo boolestá alternado: "isON deste objeto é falso". Mas quando eu pergunto o instantâneo, ok, o que você tem? ele me diz que seu único identificador de item bool ainda é verdadeiro .
E que é o que eu estou perguntando. Se o instantâneo não vai pegar o novo valor de bool, para que serve reloadItemsem primeiro lugar?
Obviamente, eu poderia apenas substituir um UniBool diferente , ou seja, um com um UUID diferente. Mas então eu não posso ligar reloadItems; travamos porque o UniBool ainda não está nos dados. Posso contornar isso chamando insertseguido de remove, e é exatamente assim que faço para contornar isso.
Mas minha pergunta é: então, para que serve reloadItems, senão exatamente para isso?
Respostas
Com base em seu novo código de exemplo, concordo, parece um bug. Quando você adiciona a reloadItemsa um instantâneo, ele aciona corretamente o fechamento da fonte de dados para solicitar uma célula atualizada, mas o IdentifierTypeitem que é passado para o fechamento é o original, não o novo valor que foi fornecido com a reloadItemschamada.
Se eu alterei sua UniBoolestrutura para uma classe para que seja uma referência em vez de um tipo de valor, as coisas funcionaram conforme o esperado (uma vez que agora há uma única instância de a em UniBoolvez de uma nova com o mesmo identificador).
Parece que, no momento, existem algumas soluções possíveis:
- Use uma referência em vez de um tipo de valor para o
IdentifierType - Use um armazenamento de apoio adicional, como um array, e acesse-o por meio
indexPathdo encerramento da fonte de dados.
Não acho que nenhum desses seja o ideal.
Curiosamente, depois que mudei UniBoolpara uma classe, tentei criar uma nova instância UniBoolque tivesse o mesmo uuidque a instância existente e recarregá-la; O código travou com uma exceção informando o identificador de item inválido especificado para recarregar ; Isso não parece certo para mim; Apenas o hashValuedeve importar, não a referência do objeto real. Tanto o objeto original quanto o novo tiveram o mesmo hashValuee ==retornaram true.
Resposta original
reloadItems funciona, mas existem dois pontos importantes:
Você deve começar com o atual da fonte de dados
snapshote chamáreloadItems-lo. Você não pode criar um novo instantâneo.Você não pode contar com o
itempassado para oCellProviderfechamento para outra coisa senãoidentifier- Ele não representa os dados mais recentes de seu modelo de apoio (matriz).
O ponto 2 significa que você precisa usar o fornecido indexPathou item.idobter seu objeto atualizado de seu modelo.
Criei um exemplo simples que exibe a hora atual em uma linha da tabela; Esta é a estrutura da fonte de dados:
struct RowData: Hashable {
var id: UUID = UUID()
var name: String
private let possibleColors: [UIColor] = [.yellow,.orange,.cyan]
var timeStamp = Date()
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
static func ==(lhs: RowData, rhs: RowData) -> Bool {
return lhs.id == rhs.id
}
}
Observe que, apesar de a hashfunção usar apenas a idpropriedade, também é necessário substituí-la ==ou você terá uma falha com um identificador inválido ao tentar recarregar a linha.
A cada segundo, uma seleção aleatória de linhas é recarregada. Ao executar o código, você verá que a hora é atualizada nessas linhas selecionadas aleatoriamente.
Este é o código que usa reloadItems:
self.timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { (timer) in
guard let datasource = self.tableview.dataSource as? UITableViewDiffableDataSource<Section,RowData> else {
return
}
var snapshot = datasource.snapshot()
var rowIdentifers = Set<RowData>()
for _ in 0...Int.random(in: 1...self.arrItems.count) {
let randomIndex = Int.random(in: 0...self.arrItems.count-1)
self.arrItems[randomIndex].timeStamp = Date()
rowIdentifers.insert(self.arrItems[randomIndex])
}
snapshot.reloadItems(Array(rowIdentifers))
datasource.apply(snapshot)
}
Eu postei a mesma pergunta , sem perceber. Eu fiz isso funcionar primeiro convertendo meu modelo em classes. Em seguida, chamando 'applySnapshot' depois de chamar 'reloadItems'.
func toggleSelectedStateForItem(at indexPath: IndexPath, animate: Bool = true) {
let item = dataSource.itemIdentifier(for: indexPath)!
var snapshot = dataSource.snapshot()
item.isSelected = !item.isSelected
snapshot.reloadItems([item])
dataSource.apply(snapshot)
}
(Eu registrei um bug sobre o comportamento demonstrado na pergunta, porque não acho que seja um bom comportamento. Mas, do jeito que as coisas estão, acho que posso dar um palpite sobre o que a ideia se destina a ser.)
Quando você informa um instantâneo de reloadum determinado item, ele não lê os dados do item fornecido! Ele simplesmente olha para o item, como uma forma de identificar qual item, já na fonte de dados, você está pedindo para recarregar.
(Portanto, se o item que você fornece é equivalente a, mas não 100% idêntico ao item já na fonte de dados, a "diferença" entre o item que você fornece e o item já na fonte de dados não importa ; os dados a fonte nunca será informada de que algo está diferente.)
Quando você envia applyesse instantâneo para a fonte de dados, a fonte de dados informa a exibição de tabela para recarregar a célula correspondente. Isso resulta na função de provedor de células da fonte de dados sendo chamada novamente.
OK, então a função de provedor de células da fonte de dados é chamada, com os três parâmetros usuais - a exibição de tabela, o caminho do índice e os dados da fonte de dados. Mas acabamos de dizer que os dados da fonte de dados não mudaram. Então, de que adianta recarregar?
A resposta é, aparentemente, que se espera que a função de provedor de célula procure em outro lugar para obter (pelo menos alguns dos) novos dados a serem exibidos na célula recém-removida da fila. Espera-se que você tenha algum tipo de "armazenamento de apoio" para o provedor de celular. Por exemplo, você pode manter um dicionário onde a chave é o tipo de identificador de célula e o valor é a informação extra que pode ser recarregada.
Isso deve ser legal, porque por definição o tipo de identificador de célula é Hashable e pode, portanto, servir como uma chave de dicionário e, além disso, os identificadores de célula devem ser exclusivos dentro dos dados, ou a fonte de dados rejeitaria os dados (por travamento). E a consulta será instantânea, pois se trata de um dicionário.
Aqui está um exemplo de trabalho completo que você pode simplesmente copiar e colar direto em um projeto. A tabela mostra três nomes junto com uma estrela que o usuário pode tocar para preencher ou esvaziar a estrela, indicando favorito ou não favorito. Os nomes são armazenados na fonte de dados diffable, mas o status de favorito é armazenado no armazenamento de apoio externo.
extension UIResponder {
func next<T:UIResponder>(ofType: T.Type) -> T? {
let r = self.next
if let r = r as? T ?? r?.next(ofType: T.self) {
return r
} else {
return nil
}
}
}
class TableViewController: UITableViewController {
var backingStore = [String:Bool]()
var datasource : UITableViewDiffableDataSource<String,String>!
override func viewDidLoad() {
super.viewDidLoad()
let cellID = "cell"
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellID)
self.datasource = UITableViewDiffableDataSource<String,String>(tableView:self.tableView) {
tableView, indexPath, name in
let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath)
var config = cell.defaultContentConfiguration()
config.text = name
cell.contentConfiguration = config
var accImageView = cell.accessoryView as? UIImageView
if accImageView == nil {
let iv = UIImageView()
iv.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(self.starTapped))
iv.addGestureRecognizer(tap)
cell.accessoryView = iv
accImageView = iv
}
let starred = self.backingStore[name, default:false]
accImageView?.image = UIImage(systemName: starred ? "star.fill" : "star")
accImageView?.sizeToFit()
return cell
}
var snap = NSDiffableDataSourceSnapshot<String,String>()
snap.appendSections(["Dummy"])
let names = ["Manny", "Moe", "Jack"]
snap.appendItems(names)
self.datasource.apply(snap, animatingDifferences: false)
names.forEach {
self.backingStore[$0] = false
}
}
@objc func starTapped(_ gr:UIGestureRecognizer) {
guard let cell = gr.view?.next(ofType: UITableViewCell.self) else {return}
guard let ip = self.tableView.indexPath(for: cell) else {return}
guard let name = self.datasource.itemIdentifier(for: ip) else {return}
guard let isFavorite = self.backingStore[name] else {return}
self.backingStore[name] = !isFavorite
var snap = self.datasource.snapshot()
snap.reloadItems([name])
self.datasource.apply(snap, animatingDifferences: false)
}
}