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