Do czego służy NSDiffableDataSnapshot `reloadItems`?

Sep 26 2020

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

2 Paulw11 Sep 27 2020 at 21:22

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ść:

  1. Użyj odwołania zamiast typu wartości dla IdentifierType
  2. 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:

  1. Musisz zacząć od prądu źródła danych snapshoti skorzystać reloadItemsz niego. Nie możesz utworzyć nowej migawki.

  2. Nie możesz polegać na itemprzekazanym do CellProviderzamknię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)
}
1 user9400730 Nov 12 2020 at 11:53

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)
}
matt Oct 01 2020 at 23:41

(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)
    }
}