swiftdiffabledatasource

Applying snapshot does not empty reconfiguredItemIdentifiers


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?


Solution

  • 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.