A cosa serve NSDiffableDataSourceSnapshot `reloadItems`?
Ho difficoltà a trovare l'uso di NSDiffableDataSourceSnapshot reloadItems(_:):
Se l'elemento che chiedo di ricaricare non è equiparabile a un elemento già presente nell'origine dati, vado in crash.
Ma se l'elemento è equiparabile a un elemento che è già presente nell'origine dati, allora che senso ha "ricaricarlo"?
Potresti pensare che la risposta al secondo punto sia: beh, potrebbe esserci qualche altro aspetto dell'oggetto identificatore dell'elemento che non fa parte della sua equiparabilità ma si riflette nell'interfaccia della cella. Ma quello che trovo è che non è vero; dopo la chiamata reloadItems, la vista tabella non riflette la modifica.
Quindi, quando voglio cambiare un elemento, quello che finisco per fare con l'istantanea è un insertdopo l'elemento da sostituire e quindi un deleteelemento originale. Non esiste un replacemetodo snapshot , che è quello che speravo reloadItemssarebbe risultato.
(Ho fatto una ricerca di Stack Overflow su questi termini e ho trovato molto poco, per lo più solo un paio di domande che lasciavano perplessi usi particolari di reloadItems, come Come aggiornare una cella di tabella usando UITableView diffable . , quale uso pratico ha trovato qualcuno di questo metodo?)
Bene, non c'è niente come avere un esempio riproducibile minimale con cui giocare, quindi eccone uno.
Crea un semplice progetto iOS vanilla con il suo modello ViewController e aggiungi questo codice al ViewController.
Lo prendo pezzo per pezzo. Innanzitutto, abbiamo una struttura che servirà come identificatore del nostro articolo. L'UUID è la parte unica, quindi equabilità e hashability dipendono solo da essa:
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
}
}
Successivamente, la vista tabella (falsa) e l'origine dati diffable:
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)
}
Quindi c'è solo un UniBool nella nostra fonte di dati diffable ed boolè true. Quindi ora imposta un pulsante per chiamare questo metodo di azione che cerca di alternare il boolvalore utilizzando 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)
}
}
}
Quindi ecco il punto. Ho detto a reloadItemscon un elemento il cui UUID è una corrispondenza, ma il cui boolè attivato: "isON di questo oggetto è falso". Ma quando chiedo l'istantanea, ok, cos'hai? mi dice che il suo unico identificatore di elemento bool è ancora vero .
E che è quello che sto chiedendo. Se l'istantanea non riprenderà il nuovo valore di bool, a cosa serve reloadItems?
Ovviamente potrei semplicemente sostituire un UniBool diverso , cioè uno con un UUID diverso. Ma poi non posso chiamare reloadItems; andiamo in crash perché UniBool non è già nei dati. Posso aggirare il problema chiamando insertseguito da remove, ed è esattamente come lo aggiro.
Ma la mia domanda è: allora a cosa serve reloadItems, se non proprio per questo?
Risposte
Sulla base del tuo nuovo codice di esempio, sono d'accordo, sembra un bug. Quando si aggiunge un reloadItemsa un'istantanea, si attiva correttamente IdentifierTypela chiusura dell'origine dati per richiedere una cella aggiornata, ma l' elemento passato alla chiusura è l'originale, non il nuovo valore fornito con la reloadItemschiamata.
Se ho cambiato la tua UniBoolstruttura in una classe in modo che sia un riferimento piuttosto che un tipo di valore, le cose hanno funzionato come previsto (poiché ora c'è una singola istanza di a UniBoolpiuttosto che una nuova con lo stesso identificatore).
Sembra che al momento ci siano un paio di possibili soluzioni:
- Utilizza un riferimento anziché un tipo di valore per
IdentifierType - Utilizzare un archivio di backup aggiuntivo, ad esempio un array, e accedervi tramite
indexPathla chiusura dell'origine dati.
Non penso che nessuno di questi sia l'ideale.
È interessante notare che, dopo essere passato UniBoola una classe, ho provato a creare una nuova istanza UniBoolche aveva lo stesso uuiddell'istanza esistente e a ricaricarla; Il codice si è bloccato con un'eccezione che indicava l' identificatore di elemento non valido specificato per il ricaricamento ; Questo non mi suona bene; Solo il hashValuedovrebbe essere importante, non il riferimento all'oggetto effettivo. Sia gli oggetti originali che quelli nuovi avevano lo stesso hashValuee ==restituiti true.
Risposta originale
reloadItems funziona, ma ci sono due punti importanti:
È necessario iniziare con la corrente dell'origine dati
snapshote richiamarlareloadItems. Non puoi creare una nuova istantanea.Non puoi fare affidamento sul
itempassaggio allaCellProviderchiusura per qualcosa di diverso daidentifier- Non rappresenta i dati più recenti dal tuo modello di supporto (array).
Il punto 2 significa che è necessario utilizzare l'oggetto fornito indexPatho item.idper ottenere l'oggetto aggiornato dal modello.
Ho creato un semplice esempio che mostra l'ora corrente in una riga della tabella; Questa è la struttura dell'origine dati:
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
}
}
Si noti che nonostante la hashfunzione utilizzi solo la idproprietà, è anche necessario eseguire l'override ==o si verificherà un arresto anomalo con un identificatore non valido quando si tenta di ricaricare la riga.
Ogni secondo viene ricaricata una selezione casuale di righe. Quando esegui il codice, vedi che l'ora viene aggiornata su quelle righe selezionate casualmente.
Questo è il codice che utilizza 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)
}
Ho postato la stessa domanda , senza rendermene conto. Ho ottenuto questo funzionamento convertendo in primo luogo il mio modello in classi. Quindi chiamare "applySnapshot" dopo aver chiamato "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)
}
(Ho segnalato un bug sul comportamento dimostrato nella domanda, perché non penso sia un buon comportamento. Ma, allo stato attuale, penso di poter fornire un'ipotesi su quale sia l'idea che dovrebbe essere.)
Quando racconti un'istantanea a reloadun determinato articolo, non viene letto nei dati dell'articolo che fornisci! Guarda semplicemente l'elemento, come un modo per identificare quale elemento, già nell'origine dati, stai chiedendo di ricaricare.
(Quindi, se l'articolo fornito è Pari a ma non identico al 100% all'articolo già nell'origine dati, la "differenza" tra l'articolo fornito e l'articolo già nell'origine dati non avrà alcuna importanza ; i dati alla fonte non verrà mai detto che qualcosa è diverso.)
Quando si applyesegue quindi l' istantanea nell'origine dati, l'origine dati indica alla vista tabella di ricaricare la cella corrispondente. Ciò si traduce nella chiamata di nuovo della funzione del provider di celle dell'origine dati .
OK, quindi viene chiamata la funzione del provider di celle dell'origine dati, con i soliti tre parametri: la vista tabella, il percorso dell'indice e i dati dell'origine dati. Ma abbiamo appena detto che i dati dell'origine dati non sono cambiati. Allora qual è lo scopo di ricaricare?
La risposta è, apparentemente, che la funzione del provider di celle dovrebbe cercare altrove per ottenere (almeno alcuni) i nuovi dati da visualizzare nella cella appena rimossa dalla coda. Ci si aspetta che tu abbia una sorta di "archivio di supporto" che il provider di celle guarda. Ad esempio, potresti mantenere un dizionario in cui la chiave è il tipo di identificatore di cella e il valore è l'informazione extra che potrebbe essere ricaricata.
Questo deve essere legale, perché per definizione il tipo di identificatore di cella è Hashable e può quindi servire come chiave del dizionario, e inoltre gli identificatori di cella devono essere univoci all'interno dei dati, altrimenti l'origine dati rifiuterebbe i dati (bloccandosi). E la ricerca sarà istantanea, perché questo è un dizionario.
Ecco un esempio funzionante completo che puoi semplicemente copiare e incollare direttamente in un progetto. La tabella mostra tre nomi insieme a una stella che l'utente può toccare per riempire o svuotare la stella, indicando preferito o non preferito. I nomi vengono memorizzati nella sorgente dati diffable, ma lo stato preferito viene memorizzato nel backing store esterno.
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)
}
}