Swift Diffabledatasource Make Insert&Delete Instead of Reload

Swift DiffableDataSource make insert&delete instead of reload

It's because you implemented Hashable incorrectly.

Remember, Hashable also means Equatable — and there is an inviolable relationship between the two. The rule is that two equal objects must have equal hash values. But in your ViewModel, "equal" involves comparing all three properties, id, title, and subtitle — even though hashValue does not, because you implemented hash.

In other words, if you implement hash, you must implement == to match it exactly:

struct ViewModel: Hashable {
let id: Int
let title: String
let subtitle: String

func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static func ==(lhs: ViewModel, rhs: ViewModel) -> Bool {
return lhs.id == rhs.id
}
}

If you make that change, you'll find that the table view animation behaves as you expect.

If you also want the table view to pick up on the fact that the underlying data has in fact changed, then you also have to call reloadData:

    diffableDataSource?.apply(snapshot, animatingDifferences: true) {
self.tableView.reloadData()
}

(If you have some other reason for wanting ViewModel's Equatable to continue involving all three properties, then you need two types, one for use when performing equality comparisons plain and simple, and another for contexts where Hashable is involved, such as diffable data sources, sets, and dictionary keys.)

How can I reload items without removing and inserting with UITableViewDiffableDataSource?

The proper solution to this is actually in the names of the APIs - the objects you give to the data source should be identifiers, like rowid values from a database. In my case, when the item identifiers don't represent rows in a database that I can look up, I just need to keep the state of the objects in some sort of lookup structure, so that when I call reloadItemsWithIdentifiers, I get the state for each cell from that structure, not from the object that the data source hands to me.

What is NSDiffableDataSourceSnapshot `reloadItems` for?

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

Stop Diffable Data Source scrolling to top after refresh

The source of this problem is probably your Item identifier type - the UserComment.

Diffable data source uses the hash of your item identifier type to detect if it is a new instance or an old one which is represented currently.
If you implement Hashable protocol manually, and you use a UUID which is generated whenever a new instance of the type is initialized, this misguides the Diffable data source and tells it this is a new instance of item identifier. So the previous ones must be deleted and the new ones should be represented. This causes the table or collection view to scroll after applying snapshot.
To solve that replace the uuid with one of the properties of the type that you know is unique or more generally use a technique to generate the same hash value for identical instances.

So to summarize, the general idea is to pass instances of the item identifiers with the same hash values to the snapshot to tell the Diffable data source that these items are not new and there is no need to delete previous ones and insert these ones. In this case you will not encounter unnecessary scrolls.

Why is UICollectionViewDiffableDataSource reloading every cell when nothing has changed?

I think you've put your finger on it. When you say animatingDifferences is to be false, you are asking the diffable data source to behave as if it were not a diffable data source. You are saying: "Skip all that diffable stuff and just accept this new data." In other words, you are saying the equivalent of reloadData(). No new cells are created (it's easy to prove that by logging), because all the cells are already visible; but by the same token, all the visible cells are reconfigured, which is exactly what one expects from saying reloadData().

When animatingDifferences is true, on the other hand, the diffable data source thinks hard about what has changed, so that, if necessary, it can animate it. As a result of all that work behind the scenes, therefore, it knows when it can avoid reloading a cell if it doesn't have to (because it can move the cell instead).

Indeed, when animatingDifferences is true, you can apply a snapshot that reverses the cells, and yet configure is never called again, because moving the cells around is all that needs to be done:

func updateSnapshot(animatingChange: Bool = true) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.all])
self.items = self.items.reversed()
snapshot.appendItems(self.items, toSection: .all)
self.dataSource.apply(snapshot, animatingDifferences: animatingChange)
}

Interestingly, I also tried the above with shuffled instead of reversed, and I found that sometimes some cells are reconfigured. Evidently it is not the main intention of the diffable data source not to reload cells; it's just a sort of side effect.

DiffableDataSource - No animation, but flickering during delete operation

Currently, instead of using enum, we are using struct to represent Section.

The reason is that, we have a dynamic content footer. Using struct enables us to carry the dynamic content information for the footer.

Our initial Section class looks as following

import Foundation

struct TabInfoSection {
var tabInfos: [TabInfo]
var footer: String
}

extension TabInfoSection: Hashable {
}

However, this is a mistake. As, we include content items TabInfo as member of Section.

When there is any mutable operation performed on content items, this is causing Diff framework to throw away entire current Section, and replace it with new Section. (Because Diff framework detects there is change in Section).

This causes flickering effect.

The correct implementation should be

import Foundation

struct TabInfoSection {
var footer: String
}

extension TabInfoSection: Hashable {
}

P/s But, this is causing additional problem, when we want to update footer explicitly. I describe it in another question - How to update footer in Section via DiffableDataSource without causing flickering effect?

Can UITableViewDiffableDataSource detect an item changed?

After almost one day's clueless experiments, I believe I figured out how diffable data source worked and solved my issue based on that understanding (it turned out my original thought was almost correct).

Diffable data source uses item hash to identify item. For the same item that exists in both old and new snapshots, diffable data source checks if the item changes by doing an "==" operation with its old and new values.

Once figured out, it looks like quite obvious and simple approach. But it's so fundamental that I can't understand why it isn't mentioned explicitly anywhere.

So, to answer my original question, yes, diffable data source can detect item value change. That said, it becomes tricky when item value is of reference type and/or the text shown in row is, say, properties of objects referenced by that object (e.g., relationship in Core Data), etc.

Another note. Whether using entire item struct or just part of it to generate item hash doesn't matter, as long as it identifies the item. I prefer to using only the essential part of the item which really identifies it.



Related Topics



Leave a reply



Submit