Do czego służy NSDiffableDataSnapshot `reloadItems`?
Mam trudności ze znalezieniem użycia NSDiffableDataSourceSnapshot reloadItems(_:):
Jeśli element, o który proszę o ponowne załadowanie, nie jest porównywalny z elementem, który jest już obecny w źródle danych, następuje awaria.
Ale jeśli element można porównać z elementem, który jest już obecny w źródle danych, to jaki jest sens jego „ponownego ładowania”?
Możesz pomyśleć, że odpowiedź na drugi punkt brzmi: cóż, może istnieć jakiś inny aspekt obiektu identyfikatora pozycji, który nie jest częścią jego równoważności, ale odzwierciedla interfejs komórki. Ale stwierdzam, że to nieprawda; po wywołaniu reloadItemswidok tabeli nie odzwierciedla zmiany.
Więc kiedy chcę zmienić element, to, co ostatecznie robię z migawką, to insertpo elemencie do wymiany, a następnie deletena oryginalnym elemencie. Nie ma replacemetody migawki , a miałem nadzieję reloadItems, że tak się stanie.
(Przeszukałem te terminy w stosie i znalazłem bardzo niewiele - głównie kilka pytań, które zaintrygowały poszczególne zastosowania reloadItems, np. Jak zaktualizować komórkę tabeli przy użyciu diffable UITableView . Pytam więc w bardziej uogólnionej formie jakie praktyczne zastosowanie znalazł ktoś dla tej metody?)
Cóż, nie ma to jak mieć minimalny odtwarzalny przykład do zabawy, więc oto jeden.
Utwórz zwykły projekt waniliowy iOS z jego szablonem ViewController i dodaj ten kod do ViewController.
Zrobię to kawałek po kawałku. Najpierw mamy strukturę, która posłuży jako nasz identyfikator przedmiotu. UUID jest unikalną częścią, więc równoważność i hashability zależą tylko od niego:
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
}
}
Następnie (fałszywy) widok tabeli i zróżnicowane źródło danych:
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)
}
Więc jest tylko jeden UniBool w naszym zróżnicowanym źródle danych i booljest true. Skonfiguruj teraz przycisk wywołujący tę metodę akcji, która próbuje przełączyć boolwartość za pomocą 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)
}
}
}
Więc o to chodzi. Powiedziałem reloadItemso elemencie, którego UUID jest zgodny, ale którego booljest przełączane: "isON tego obiektu jest fałszywe". Ale kiedy pytam o migawkę, ok, co masz? mówi mi, że jego jedyny identyfikator pozycji bool jest nadal prawdziwy .
I o to właśnie pytam. Jeśli migawka nie uzyska nowej wartości bool, po co reloadItemsw pierwszej kolejności?
Oczywiście mogłem po prostu zastąpić inny UniBool, czyli taki z innym UUID. Ale wtedy nie mogę zadzwonić reloadItems; Mamy awarię, ponieważ tego UniBool nie ma już w danych. Mogę to obejść, dzwoniąc, inserta następnie remove, i dokładnie tak to robię.
Ale moje pytanie brzmi: po co więc reloadItems, jeśli nie po to?
Odpowiedzi
Na podstawie twojego nowego przykładowego kodu zgadzam się, wygląda to na błąd. Po dodaniu reloadItemsdo migawki poprawnie wyzwala zamknięcie źródła danych w celu zażądania zaktualizowanej komórki, ale IdentifierTypeelement przekazany do zamknięcia jest oryginałem, a nie nową wartością dostarczoną z reloadItemswywołaniem.
Jeśli UniBoolzmienię twoją strukturę na klasę, tak aby była referencją, a nie typem wartości, to wszystko działało zgodnie z oczekiwaniami (ponieważ istnieje teraz pojedyncze wystąpienie a UniBoolzamiast nowego z tym samym identyfikatorem).
Wydaje się, że w tej chwili istnieje kilka możliwych obejść:
- Użyj odwołania zamiast typu wartości dla
IdentifierType - Użyj dodatkowego magazynu zapasowego, takiego jak tablica, i uzyskaj do niego dostęp za pośrednictwem
indexPathzamknięcia źródła danych.
Nie sądzę, aby którykolwiek z nich był idealny.
Co ciekawe, po UniBoolprzejściu na klasę próbowałem utworzyć nową instancję, UniBoolktóra miała to samo, uuidco instancja istniejąca i ponownie ją załadować; Kod uległ awarii z wyjątkiem stwierdzającym, że do ponownego załadowania określono nieprawidłowy identyfikator pozycji ; To nie brzmi dla mnie dobrze; Tylko hashValuepowinno mieć znaczenie, a nie rzeczywiste odniesienie do obiektu. Zarówno oryginalne, jak i nowe obiekty miały to samo hashValuei ==wróciły true.
Oryginalna odpowiedź
reloadItems działa, ale są dwa ważne punkty:
Musisz zacząć od prądu źródła danych
snapshoti skorzystaćreloadItemsz niego. Nie możesz utworzyć nowej migawki.Nie możesz polegać na
itemprzekazanym doCellProviderzamknięcia niczego innego niżidentifier- Nie reprezentuje najnowszych danych z modelu zapasowego (tablicy).
Punkt 2 oznacza, że musisz użyć dostarczonego indexPathlub item.iduzyskać zaktualizowany obiekt ze swojego modelu.
Stworzyłem prosty przykład, który wyświetla aktualny czas w wierszu tabeli; Oto struktura źródła danych:
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
}
}
Zauważ, że pomimo hashfunkcji używającej tylko idwłaściwości, konieczne jest również nadpisanie, w ==przeciwnym razie wystąpi awaria z nieprawidłowym identyfikatorem podczas próby ponownego załadowania wiersza.
Co sekundę losowo wybrane wiersze są ponownie ładowane. Po uruchomieniu kodu zobaczysz, że czas jest aktualizowany w tych losowo wybranych wierszach.
Oto kod, który używa 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)
}
Wysłałem to samo pytanie , nie zdając sobie sprawy. Mam to działające, najpierw konwertując mój model na klasy. Następnie wywołanie „applySnapshot” po wywołaniu „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)
}
(Zgłosiłem błąd dotyczący zachowania zademonstrowanego w pytaniu, ponieważ uważam, że nie jest to dobre zachowanie. Ale w obecnym stanie rzeczy myślę, że mogę zgadnąć, jaki ma być pomysł.)
Kiedy powiesz migawkę reloadokreślonej pozycji, nie wczyta ona danych dostarczonego przedmiotu! To po prostu wygląda na element, jako sposób identyfikacji , jaki przedmiot, już w źródle danych, prosicie, aby przeładować.
(Tak więc, jeśli dostarczany przedmiot jest równoważny, ale nie w 100% identyczny z elementem znajdującym się już w źródle danych, „różnica” między dostawą a pozycją znajdującą się już w źródle danych nie będzie miała żadnego znaczenia ; dane źródło nigdy nie dowie się, że coś jest inne).
Kiedy następnie applywykonujesz ten zrzut w źródle danych, źródło danych nakazuje widokowi tabeli ponowne załadowanie odpowiedniej komórki. Powoduje to ponowne wywołanie funkcji dostawcy komórki źródła danych .
OK, więc wywoływana jest funkcja dostawcy komórek źródła danych ze zwykłymi trzema parametrami - widokiem tabeli, ścieżką indeksu i danymi ze źródła danych. Ale właśnie powiedzieliśmy, że dane ze źródła danych nie uległy zmianie. Więc jaki jest w ogóle sens przeładowywania?
Najwyraźniej odpowiedź brzmi, że funkcja dostawcy komórki ma szukać gdzie indziej, aby pobrać (przynajmniej część) nowych danych do wyświetlenia w nowo usuniętej z kolejki komórce. Oczekuje się, że będziesz mieć jakiś „magazyn zapasowy”, na który patrzy dostawca komórek. Na przykład możesz utrzymywać słownik, w którym kluczem jest typ identyfikatora komórki, a wartością są dodatkowe informacje, które mogą zostać ponownie załadowane.
Musi to być zgodne z prawem, ponieważ z definicji typ identyfikatora komórki jest Hashable i dlatego może służyć jako klucz słownikowy, a ponadto identyfikatory komórek muszą być unikalne w danych, w przeciwnym razie źródło danych odrzuciłoby dane (przez awarię). Wyszukiwanie będzie natychmiastowe, ponieważ to jest słownik.
Oto kompletny przykład roboczy, który możesz po prostu skopiować i wkleić bezpośrednio do projektu. Tabela przedstawia trzy nazwy wraz z gwiazdką, którą użytkownik może dotknąć, aby gwiazdka była wypełniona lub pusta, wskazując na ulubione lub niepochlebne. Nazwy są przechowywane w zróżnicowanym źródle danych, ale ulubiony status jest przechowywany w zewnętrznym magazynie zapasowym.
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)
}
}