I'm using UITableViewDiffableDataSource in my project with UIKit. In some cases (e.g. moving item from one place to another) I need to use .reconfigureItems(_:)
or .reloadItems(_:)
to force DiffableDataSource to update certain cells. But the result of using those methods can lead to updating all items in UITableView. I found that when I'm applying snapshot using .apply(_:)
all the previous items are still in the snapshot.reconfiguredItemIdentifiers
property.
Here is a code example of setting up UITableViewDiffableDataSource and reproducing the problem.
class ViewController: UIViewController {
@IBOutlet var tableView: UITableView!
var activitiesDataSource: [Activity]!
var snapshot = NSDiffableDataSourceSnapshot<Int, Activity.ID>()
lazy var diffableDataSource = UITableViewDiffableDataSource<Int, Activity.ID>(tableView: tableView) { tableView, indexPath, itemIdentifier in
let activity = self.getActivity(by: itemIdentifier)
print("setting up cell with title: \(activity.title)")
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.titleLabel.text = activity.title
return cell
}
override func viewDidLoad() {
super.viewDidLoad()
activitiesDataSource = [Activity(title: "first"), Activity(title: "second"), Activity(title: "third")]
tableView.delegate = self
tableView.dataSource = diffableDataSource
tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: "CustomCell")
prepareSnapshot()
}
func prepareSnapshot() {
let activitiesIds = activitiesDataSource.map(\.id)
snapshot.appendSections([0])
snapshot.appendItems(activitiesIds)
diffableDataSource.apply(snapshot)
}
func getActivity(by id: UUID) -> Activity {
guard let activity = self.activitiesDataSource.first(where: { $0.id == id }) else {
fatalError("Could not find activity by id")
}
return activity
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let activityId = snapshot.itemIdentifiers[indexPath.row]
let activity = getActivity(by: activityId)
activity.title += " tap"
snapshot.reconfigureItems([activityId])
diffableDataSource.apply(snapshot)
}
}
And Activity
class looks like:
class Activity: Identifiable {
let id = UUID()
var title: String
init(title: String) {
self.title = title
}
}
If you tap first, second and third cells, the output will be kind of:
setting up cell with title: first tap
setting up cell with title: first tap
setting up cell with title: second tap
setting up cell with title: first tap
setting up cell with title: second tap
setting up cell with title: third tap
While I was expecting to have:
setting up cell with title: first tap
setting up cell with title: second tap
setting up cell with title: third tap
Can someone help me and explain what am I doing wrong?
Try creating a new snapshot from the previous one instead of directly updating the property-stored one as it's.
https://developer.apple.com/documentation/uikit/nsdiffabledatasourcesnapshot
You can create and configure a snapshot in one of these ways: Create an empty snapshot, then append sections and items to it. Get the current snapshot by calling the diffable data source’s snapshot() method, then modify that snapshot to reflect the new state of the data that you want to display.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
var snapshot = diffableDataSource.snapshot()
let activityId = snapshot.itemIdentifiers[indexPath.row]
let activity = getActivity(by: activityId)
activity.title += " tap"
snapshot.reconfigureItems([activityId])
diffableDataSource.apply(snapshot)
}
Hopefully this solves your problem, if not then also consider having your model Activity
conform to Identifiable and Hashable
instead of using the UUID
directly as hashable.
...
lazy var diffableDataSource = UITableViewDiffableDataSource<Int, Activity>(tableView: tableView) { tableView, indexPath, itemIdentifier in
...
If it persists, adding your own version of the Equatable
method ==(lhs:rhs)
may help the diffing algorithm properly identify your cells.
static func ==(lhs: WeekAnime, rhs: WeekAnime) -> Bool {
// Your comparable condition.
// like lhs.id == rhs.id
}
Btw Hashable
already conforms to Equatable
being the scenes.