Wofür ist NSDiffableDataSourceSnapshot `reloadItems`?
Ich habe Schwierigkeiten, die Verwendung von NSDiffableDataSourceSnapshot zu finden reloadItems(_:):
Wenn das Element, das ich neu laden möchte, nicht mit einem Element vergleichbar ist, das bereits in der Datenquelle vorhanden ist, stürze ich ab.
Wenn das Element jedoch mit einem Element vergleichbar ist, das bereits in der Datenquelle vorhanden ist, wozu muss es dann "neu geladen" werden?
Sie könnten denken, die Antwort auf den zweiten Punkt lautet: Nun, es könnte einen anderen Aspekt des Objektkennungsobjekts geben, der nicht Teil seiner Gleichwertigkeit ist, sondern sich in der Zellenschnittstelle widerspiegelt. Aber ich finde, dass das nicht stimmt; Nach dem Aufruf spiegelt die reloadItemsTabellenansicht die Änderung nicht wider.
Wenn ich also einen Artikel ändern möchte, mache ich am Ende mit dem Schnappschuss ein insertnach dem zu ersetzenden Element und dann ein deletenach dem ursprünglichen Element. Es gibt keine Schnappschussmethode replace, wie ich es mir erhofft hatte reloadItems.
(Ich habe eine Stapelüberlaufsuche nach diesen Begriffen durchgeführt und nur sehr wenig gefunden - meistens nur ein paar Fragen, die über bestimmte Verwendungszwecke von verwirrten reloadItems, z. B. wie eine Tabellenzelle mit diffable UITableView aktualisiert wird . Daher stelle ich eine allgemeinere Frage , welchen praktischen Nutzen hat jemand für diese Methode gefunden?)
Es gibt nichts Schöneres, als ein minimal reproduzierbares Beispiel zum Spielen zu haben. Hier ist eines.
Erstellen Sie mit der Vorlage ViewController ein einfaches Vanille-iOS-Projekt und fügen Sie diesen Code dem ViewController hinzu.
Ich werde es Stück für Stück nehmen. Erstens haben wir eine Struktur, die als Artikelkennung dient. Die UUID ist der einzigartige Teil, daher hängen Gleichheit und Hashbarkeit allein davon ab:
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
}
}
Als nächstes die (gefälschte) Tabellenansicht und die diffible Datenquelle:
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)
}
Es gibt also nur einen UniBool in unserer diffiblen Datenquelle und das boolist es auch true. Richten Sie nun eine Schaltfläche zum Aufrufen dieser Aktionsmethode ein, mit der versucht wird, den boolWert mithilfe von Folgendem umzuschalten 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)
}
}
}
Also hier ist die Sache. Ich sagte zu reloadItemseinem Element, dessen UUID eine Übereinstimmung ist, dessen boolUmschaltung jedoch umgeschaltet ist: "Das isON dieses Objekts ist falsch". Aber wenn ich nach dem Schnappschuss frage, okay, was hast du? es sagt mir, dass die einzige Artikelkennung bool immer noch wahr ist .
Und das ist , was ich zu fragen. Wenn der Schnappschuss nicht den neuen Wert von boolaufnimmt, wofür ist er dann überhaupt reloadItemsgedacht?
Natürlich könnte ich einfach einen anderen UniBool ersetzen, dh einen mit einer anderen UUID. Aber dann kann ich nicht anrufen reloadItems; Wir stürzen ab, weil dieser UniBool noch nicht in den Daten enthalten ist. Ich kann das umgehen, indem ich anrufe, insertgefolgt von remove, und genau so arbeite ich daran.
Aber meine Frage ist: Also, wofür reloadItems, wenn nicht für genau diese Sache?
Antworten
Basierend auf Ihrem neuen Beispielcode, stimme ich zu, sieht es wie ein Fehler aus. Wenn Sie reloadItemseinem Snapshot ein hinzufügen , wird der Datenquellenschluss korrekt ausgelöst, um eine aktualisierte Zelle anzufordern. Das an den Abschluss übergebene IdentifierTypeElement ist jedoch das Original und nicht der neue Wert, der mit dem reloadItemsAufruf bereitgestellt wurde .
Wenn ich Ihre UniBoolStruktur in eine Klasse geändert habe , sodass es sich eher um eine Referenz als um einen Werttyp handelt, haben die Dinge wie erwartet funktioniert (da es jetzt eine einzelne Instanz einer UniBoolstatt einer neuen Instanz mit demselben Bezeichner gibt).
Im Moment scheint es einige mögliche Umgehungsmöglichkeiten zu geben:
- Verwenden Sie eine Referenz anstelle eines Werttyps für die
IdentifierType - Verwenden Sie einen zusätzlichen Sicherungsspeicher, z. B. ein Array, und greifen Sie über
indexPathden Datenquellenabschluss darauf zu.
Ich denke nicht, dass beides ideal ist.
Interessanterweise habe ich nach dem Wechsel UniBoolzu einer Klasse versucht, eine neue Instanz zu erstellen UniBool, die dieselbe uuidwie die vorhandene Instanz hat, und diese neu zu laden. Der Code stürzte mit einer Ausnahme ab, die angab, dass für das erneute Laden eine ungültige Elementkennung angegeben wurde . Das klingt für mich nicht richtig; Nur das hashValuesollte eine Rolle spielen, nicht die eigentliche Objektreferenz. Sowohl das ursprüngliche als auch das neue Objekt hatten das gleiche hashValueund ==kehrten zurück true.
Ursprüngliche Antwort
reloadItems funktioniert, aber es gibt zwei wichtige Punkte:
Sie müssen mit der aktuellen Datenquelle beginnen
snapshotund diese aufrufenreloadItems. Sie können keinen neuen Schnappschuss erstellen.Sie können sich nicht auf die
itemÜbergabe an denCellProviderAbschluss für etwas anderes als das verlassenidentifier- Es repräsentiert nicht die neuesten Daten aus Ihrem Hintergrundmodell (Array).
Punkt 2 bedeutet, dass Sie das bereitgestellte verwenden indexPathoder item.idIhr aktualisiertes Objekt von Ihrem Modell erhalten müssen.
Ich habe ein einfaches Beispiel erstellt , das die aktuelle Uhrzeit in einer Tabellenzeile anzeigt. Dies ist die Datenquellenstruktur:
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
}
}
Beachten Sie, dass trotz der hashFunktion, die nur die idEigenschaft verwendet, auch eine Überschreibung erforderlich ist. Andernfalls tritt ==beim erneuten Laden der Zeile ein Absturz mit einer ungültigen Kennung auf.
Jede Sekunde wird eine zufällige Auswahl von Zeilen neu geladen. Wenn Sie den Code ausführen, sehen Sie, dass die Zeit in diesen zufällig ausgewählten Zeilen aktualisiert wird.
Dies ist der Code, der Folgendes verwendet 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)
}
Ich habe die gleiche Frage gestellt , ohne es zu merken. Ich habe das funktioniert, indem ich zuerst mein Modell in Klassen konvertiert habe. Rufen Sie dann 'applySnapshot' auf, nachdem Sie 'reloadItems' aufgerufen haben.
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)
}
(Ich habe einen Fehler in dem in der Frage gezeigten Verhalten gemeldet, weil ich nicht denke, dass es ein gutes Verhalten ist. Aber aus heutiger Sicht kann ich eine Vermutung anstellen, wie die Idee aussehen soll.)
Wenn Sie reloadeinem bestimmten Artikel einen Schnappschuss mitteilen , werden die Daten des von Ihnen gelieferten Artikels nicht eingelesen! Es ist einfach schaut auf das Element, als eine Möglichkeit , zu identifizieren , was Artikel, bereits in der Datenquelle, die Sie fragen , neu zu laden.
(Also, wenn das ausgewählte Element Versorgung ist gleichzusetzen, aber nicht 100% identisch mit dem Artikel ist bereits in der Datenquelle, die „Differenz“ zwischen dem Element , das Sie liefern , und das Element bereits in der Datenquelle wird nicht an allen aus , die Daten Quelle wird nie gesagt, dass etwas anders ist.)
Wenn Sie dann applydiesen Snapshot zur Datenquelle erstellen, weist die Datenquelle die Tabellenansicht an, die entsprechende Zelle neu zu laden. Dies führt zu der Datenquelle Zelle Provider Funktion erneut aufgerufen werden.
OK, daher wird die Zellenanbieterfunktion der Datenquelle mit den üblichen drei Parametern aufgerufen - der Tabellenansicht, dem Indexpfad und den Daten aus der Datenquelle. Wir haben jedoch gerade gesagt, dass sich die Daten aus der Datenquelle nicht geändert haben. Wozu also überhaupt neu laden?
Die Antwort ist anscheinend, dass von der Zellenanbieterfunktion erwartet wird, dass sie sich anderswo umschaut, um (zumindest einige) der neuen Daten zu erhalten, die in der neu in die Warteschlange gestellten Zelle angezeigt werden sollen. Es wird erwartet, dass Sie eine Art "Backing Store" haben, den der Mobilfunkanbieter betrachtet. Beispielsweise verwalten Sie möglicherweise ein Wörterbuch, in dem der Schlüssel der Typ der Zellenkennung und der Wert die zusätzlichen Informationen sind, die möglicherweise neu geladen werden.
Dies muss legal sein, da der Zellkennungstyp per Definition Hashable ist und daher als Wörterbuchschlüssel dienen kann. Außerdem müssen die Zellkennungen innerhalb der Daten eindeutig sein, da die Datenquelle die Daten sonst ablehnen würde (durch Absturz). Und die Suche erfolgt sofort, da dies ein Wörterbuch ist.
Hier ist ein vollständiges Arbeitsbeispiel, das Sie einfach kopieren und direkt in ein Projekt einfügen können. In der Tabelle sind drei Namen zusammen mit einem Stern dargestellt, auf den der Benutzer tippen kann, um den Stern zu füllen oder zu leeren, wobei Favorit oder Nicht-Favorit angegeben wird. Die Namen werden in der diffiblen Datenquelle gespeichert, der Favoritenstatus wird jedoch im externen Sicherungsspeicher gespeichert.
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)
}
}