À quoi sert NSDiffableDataSourceSnapshot `reloadItems`?
J'ai du mal à trouver l'utilisation de NSDiffableDataSourceSnapshot reloadItems(_:):
Si l'élément que je demande de recharger n'est pas assimilable à un élément déjà présent dans la source de données, je plante.
Mais si l'élément est assimilable à un élément déjà présent dans la source de données, quel est l'intérêt de le «recharger»?
Vous pourriez penser que la réponse au deuxième point est: eh bien, il peut y avoir un autre aspect de l'objet identificateur d'élément qui ne fait pas partie de son équatabilité mais qui se reflète dans l'interface de la cellule. Mais ce que je trouve, c'est que ce n'est pas vrai; après l'appel reloadItems, la vue de table ne reflète pas la modification.
Ainsi, lorsque je veux modifier un élément, ce que je finis par faire avec l'instantané est un insertaprès l'élément à remplacer, puis un deleteélément d'origine. Il n'y a pas de replaceméthode de cliché , ce que j'espérais reloadItemsêtre.
(J'ai effectué une recherche Stack Overflow sur ces termes et j'ai trouvé très peu de choses - principalement juste quelques questions qui m'interrogeaient sur des utilisations particulières de reloadItems, telles que Comment mettre à jour une cellule de tableau à l'aide de UITableView diffable . Je pose donc la question sous une forme plus générale , quelle utilisation pratique quelqu'un a-t -il trouvé pour cette méthode?)
Eh bien, il n'y a rien de tel que d'avoir un exemple reproductible minimal avec lequel jouer, alors en voici un.
Créez un projet iOS simple avec son modèle ViewController et ajoutez ce code au ViewController.
Je vais le prendre morceau par morceau. Tout d'abord, nous avons une structure qui servira d'identifiant d'élément. L'UUID est la partie unique, donc l'équatabilité et la capacité de hachage en dépendent uniquement:
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
}
}
Ensuite, la (fausse) vue table et la source de données 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)
}
Il n'y a donc qu'un seul UniBool dans notre source de données diffable et boolc'est true. Alors maintenant, configurez un bouton pour appeler cette méthode d'action qui tente de basculer la boolvaleur en utilisant 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)
}
}
}
Alors voici la chose. J'ai dit à reloadItemsavec un élément dont l'UUID est une correspondance, mais dont boolon bascule: "isON de cet objet est faux". Mais quand je demande l'instantané, d'accord, qu'est-ce que tu as? il me dit que son seul identifiant d'article bool est toujours vrai .
Et c'est ce que je demande. Si l'instantané ne va pas prendre la nouvelle valeur de bool, à quoi sert reloadItems-il en premier lieu?
Évidemment, je pourrais simplement remplacer un UniBool différent , c'est-à-dire un avec un UUID différent. Mais alors je ne peux pas appeler reloadItems; nous plantons parce que UniBool n'est pas déjà dans les données. Je peux contourner ce problème en appelant insertsuivi de remove, et c'est exactement comme ça que je le contourner.
Mais ma question est: alors à quoi sert reloadItems, sinon pour cette chose même?
Réponses
Sur la base de votre nouvel exemple de code, je suis d'accord, cela ressemble à un bogue. Lorsque vous ajoutez un reloadItemsà un instantané, cela déclenche correctement la fermeture de la source de données pour demander une cellule mise à jour, mais l' IdentifierTypeélément qui est passé à la fermeture est l'original, pas la nouvelle valeur fournie avec l' reloadItemsappel.
Si j'ai changé votre UniBoolstructure en classe afin que ce soit une référence plutôt qu'un type valeur, alors les choses ont fonctionné comme prévu (puisqu'il y a maintenant une seule instance d'un UniBoolplutôt qu'une nouvelle avec le même identifiant).
Il semble qu'il existe pour le moment quelques solutions de contournement possibles:
- Utilisez une référence plutôt qu'un type valeur pour le
IdentifierType - Utilisez un magasin de stockage supplémentaire, tel qu'un tableau, et accédez-y via
indexPathdans la fermeture de la source de données.
Je ne pense pas que l'un ou l'autre de ces éléments soit idéal.
Fait intéressant, après avoir changé UniBoolpour une classe, j'ai essayé de créer une nouvelle instance de UniBoolcelle-ci ayant le même uuidque l'instance existante et de recharger cela; Le code s'est écrasé avec une exception indiquant l'identificateur d'élément non valide spécifié pour le rechargement ; Cela ne me semble pas correct; Seul le hashValuedevrait importe, pas la référence réelle de l'objet. Les objets d'origine et les nouveaux objets étaient identiques hashValueet ==renvoyés true.
Réponse originale
reloadItems fonctionne, mais il y a deux points importants:
Vous devez commencer par le courant de la source de données
snapshotet faire appelreloadItemsà cela. Vous ne pouvez pas créer un nouvel instantané.Vous ne pouvez pas compter sur le
itempassé à laCellProviderfermeture pour autre chose que leidentifier- Il ne représente pas les données les plus récentes de votre modèle de support (tableau).
Le point 2 signifie que vous devez utiliser le fourni indexPathou item.idobtenir votre objet mis à jour à partir de votre modèle.
J'ai créé un exemple simple qui affiche l'heure actuelle dans une ligne de tableau; Voici la structure de la source de données:
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
}
}
Notez que bien que la hashfonction n'utilise que la idpropriété, il est également nécessaire de remplacer ==ou vous obtiendrez un plantage avec un identifiant invalide lorsque vous tenterez de recharger la ligne.
Chaque seconde, une sélection aléatoire de lignes est rechargée. Lorsque vous exécutez le code, vous voyez que l'heure est mise à jour sur ces lignes sélectionnées au hasard.
C'est le code qui utilise 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)
}
J'ai posté la même question , sans m'en rendre compte. J'ai fait fonctionner cela en convertissant d'abord mon modèle en classes. Puis appelez 'applySnapshot' après avoir appelé '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)
}
(J'ai déposé un bug sur le comportement démontré dans la question, parce que je ne pense pas que ce soit un bon comportement. Mais, dans l'état actuel des choses, je pense que je peux donner une estimation de ce que l'idée est censée être.)
Lorsque vous donnez un instantané à reloadun certain élément, il ne lit pas les données de l'élément que vous fournissez! Il regarde simplement l'élément, comme un moyen d'identifier quel élément, déjà dans la source de données, vous demandez à recharger.
(Ainsi, si l'élément que vous fournissez est équivalent mais pas identique à 100% à l'élément déjà dans la source de données, la «différence» entre l'élément que vous fournissez et l'élément déjà dans la source de données n'aura aucune importance ; les données on ne dira jamais à la source que quelque chose est différent.)
Lorsque vous effectuez ensuite applycet instantané vers la source de données, la source de données indique à la vue de table de recharger la cellule correspondante. Cela entraîne le nouvel appel de la fonction de fournisseur de cellules de la source de données .
OK, donc la fonction de fournisseur de cellules de la source de données est appelée, avec les trois paramètres habituels - la vue de la table, le chemin d'index et les données de la source de données. Mais nous venons de dire que les données de la source de données n'ont pas changé. Alors, quel est l'intérêt de recharger du tout?
La réponse est, apparemment, que la fonction de fournisseur de cellules devrait chercher ailleurs pour obtenir (au moins une partie) des nouvelles données à afficher dans la cellule nouvellement retirée de la file d'attente. On s'attend à ce que vous ayez une sorte de "magasin de sauvegarde" que le fournisseur de cellules examine. Par exemple, vous pouvez gérer un dictionnaire dans lequel la clé est le type d'identificateur de cellule et la valeur les informations supplémentaires qui peuvent être rechargées.
Cela doit être légal, car par définition le type d'identifiant de cellule est hashable et peut donc servir de clé de dictionnaire, et de plus les identifiants de cellule doivent être uniques dans les données, sinon la source de données rejetterait les données (en se bloquant). Et la recherche sera instantanée, car il s'agit d'un dictionnaire.
Voici un exemple de travail complet que vous pouvez simplement copier et coller directement dans un projet. Le tableau présente trois noms avec une étoile sur laquelle l'utilisateur peut appuyer pour que l'étoile soit remplie ou vide, indiquant un favori ou non. Les noms sont stockés dans la source de données différente, mais l'état des favoris est stocké dans la mémoire de sauvegarde externe.
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)
}
}