Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is NSDiffableDataSourceSnapshot `reloadItems` for?

I'm having difficulty finding the use of NSDiffableDataSourceSnapshot reloadItems(_:):

  • If the item I ask to reload is not equatable to an item that is already present in the data source, I crash with:

    Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Attempted to reload item identifier that does not exist in the snapshot: ProjectName.ClassName

  • But if the item is equatable to an item that is already present in the data source, then what's the point of "reloading" it?

You might think the answer to the second point is: well, there might be some other aspect of the item identifier object that is not part of its equatability but does reflect into the cell interface. But what I find is that that's not true; after calling reloadItems, the table view does not reflect the change.

So when I want to change an item, what I end up doing with the snapshot is an insert after the item to be replaced and then a delete of the original item. There is no snapshot replace method, which is what I was hoping reloadItems would turn out to be.

(I did a Stack Overflow search on those terms and found very little — mostly just a couple of questions that puzzled over particular uses of reloadItems, such as How to update a table cell using diffable UITableView. So I'm asking in a more generalized form, what practical use has anyone found for this method?)


Well, there's nothing like having a minimal reproducible example to play with, so here is one.

Make a plain vanilla iOS project with its template ViewController, and add this code to the ViewController.

I'll take it piece by piece. First, we have a struct that will serve as our item identifier. The UUID is the unique part, so equatability and hashability depend upon it alone:

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

Next, the (fake) table view and the diffable data source:

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

So there is just one UniBool in our diffable data source and its bool is true. So now set up a button to call this action method which tries to toggle the bool value by using 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)         }     } } 

So here's the thing. I said to reloadItems with an item whose UUID is a match, but whose bool is toggled: "this object's isON is false". But when I ask the snapshot, okay, what have you got? it tells me that its sole item identifier's bool is still true.

And that is what I'm asking about. If the snapshot is not going to pick up the new value of bool, what is reloadItems for in the first place?

Obviously I could just substitute a different UniBool, i.e. one with a different UUID. But then I cannot call reloadItems; we crash because that UniBool is not already in the data. I can work around that by calling insert followed by remove, and that is exactly how I do work around it.

But my question is: so what is reloadItems for, if not for this very thing?

like image 815
matt Avatar asked Sep 26 '20 19:09

matt


People also ask

What is nsdiffabledatasourcesnapshot?

A representation of the state of the data in a view at a specific point in time.

What does Collectionview reloadData do?

reloadData()Reloads all of the data for the collection view.

Why can’t I get the nsmanagedobjectid from the result of a snapshot?

The main reason this is wrong is that the resulting snapshot contains a collection of NSManagedObject instances while a snapshot of NSManagedObjectID is expected. Although we could’ve created this ourselves too it’s better to just use performFetch and make the fetched results controller responsible for configuring the data.

What is the data in a snapshot?

The data in a snapshot is made up of the sections and items you want to display, in the order you that you determine. You configure what to display by adding, deleting, or moving the sections and items.

What is a snapshot in Salesforce?

A representation of the state of the data in a view at a specific point in time. Diffable data sources use snapshots to provide data for collection views and table views. You use a snapshot to set up the initial state of the data that a view displays, and you use snapshots to reflect changes to the data that the view displays.

When to apply the first snapshot of data?

Now that our data source and fetched results controller are ready we need to apply the first snapshot of data. This is often where the first mistakes are done as we tempt to approach diffable data sources with Core Data just like if we’re using it without Core Data.


1 Answers

(I've filed a bug on the behavior demonstrated in the question, because I don't think it's good behavior. But, as things stand, I think I can provide a guess as to what the idea is intended to be.)


When you tell a snapshot to reload a certain item, it does not read in the data of the item you supply! It simply looks at the item, as a way of identifying what item, already in the data source, you are asking to reload.

(So, if the item you supply is Equatable to but not 100% identical to the item already in the data source, the "difference" between the item you supply and the item already in the data source will not matter at all; the data source will never be told that anything is different.)

When you then apply that snapshot to the data source, the data source tells the table view to reload the corresponding cell. This results in the data source's cell provider function being called again.

OK, so the data source's cell provider function is called, with the usual three parameters — the table view, the index path, and the data from the data source. But we've just said that the data from the data source has not changed. So what is the point of reloading at all?

The answer is, apparently, that the cell provider function is expected to look elsewhere to get (at least some of) the new data to be displayed in the newly dequeued cell. You are expected to have some sort of "backing store" that the cell provider looks at. For example, you might be maintaining a dictionary where the key is the cell identifier type and the value is the extra information that might be reloaded.

This must be legal, because by definition the cell identifier type is Hashable and can therefore serve as a dictionary key, and moreover the cell identifiers must be unique within the data, or the data source would reject the data (by crashing). And the lookup will be instant, because this is a dictionary.


Here's a complete working example you can just copy and paste right into a project. The table portrays three names along with a star that the user can tap to make star be filled or empty, indicating favorite or not-favorite. The names are stored in the diffable data source, but the favorite status is stored in the external backing store.

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)     } } 
like image 151
matt Avatar answered Sep 23 '22 02:09

matt