Skip to content

Instantly share code, notes, and snippets.

@davbeck

davbeck/README.md

Created Jun 19, 2019
Embed
What would you like to do?
A wrapper for UICollectionViewDiffableDataSource that also handles updates to data

UICollectionViewDiffableDataSource does an excellent job of handling changes to data and updating the items accordingly. However, there seems to be no good way to handle updates to existing items. If you follow the samples that Apple provides and define Equatable and Hashable to use an id instead of the complete models value, value changes won't cause the collection view to update those cells. If you leave Equatable as it should be and have it compare all values of a model, the cell will update, but it will completely replace the old cell, causing an undesirable "flash".

UICollectionViewComparableDataSource wraps a UICollectionViewDiffableDataSource and will check for items that have been updated, but not removed or added.

This allows us to make updates to a cell without completely reloading it. This is especially usefull if your cells have some sort of temporary state.

This is just an expirement. Use at your own risk.

import UIKit
public protocol Identifiable: Hashable {
var identity: AnyHashable { get }
}
public struct UICollectionViewComparableDataSource<SectionIdentifierType: Hashable, ItemType: Identifiable> {
private struct ItemIdentifierType: Hashable, Identifiable {
var value: ItemType
init(_ value: ItemType) {
self.value = value
}
var identity: AnyHashable {
return value.identity
}
static func == (_ a: ItemIdentifierType, _ b: ItemIdentifierType) -> Bool {
return a.identity == b.identity
}
func hash(into hasher: inout Hasher) {
hasher.combine(identity)
}
}
private let dataSource: UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>
private let collectionView: UICollectionView
private let cellUpdater: CellUpdater
public typealias CellProvider = (UICollectionView, IndexPath, ItemType) -> UICollectionViewCell?
public typealias CellUpdater = (UICollectionView, UICollectionViewCell, IndexPath, ItemType) -> Void
public init(collectionView: UICollectionView, cellProvider: @escaping CellProvider, cellUpdater: @escaping CellUpdater) {
self.collectionView = collectionView
self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item -> UICollectionViewCell? in
guard let cell = cellProvider(collectionView, indexPath, item.value) else { return nil }
cellUpdater(collectionView, cell, indexPath, item.value)
return cell
}
self.cellUpdater = cellUpdater
}
public func apply(_ snapshot: NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemType>, animatingDifferences: Bool = true) {
// the items currently visible
// we shouldn't need to update anything offscreen since it will get a new cell even if it moves into view
let visibleItemIdentifiers = collectionView.indexPathsForVisibleItems
.compactMap({ dataSource.itemIdentifier(for: $0) })
// the item identifiers that are not causing a new cell to be created, but have an updated value
var updatedItemIdentifiers: [ItemIdentifierType] = []
// we need to convert the snapshot types, even though this is sort of a no-op
let converted = NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>()
for section in snapshot.sectionIdentifiers {
converted.appendSections([section])
let itemIdentifiers = snapshot.itemIdentifiers(inSection: section).map({ ItemIdentifierType($0) })
// any of these items that are in the visibleItemIdentifiers (defined by identity) but have a different value
updatedItemIdentifiers.append(contentsOf: itemIdentifiers
.filter({ itemIdentifier in
visibleItemIdentifiers.first(where: { $0 == itemIdentifier })?.value != itemIdentifier.value
}))
converted.appendItems(itemIdentifiers, toSection: section)
}
let updates = {
// only updating the cells that have changed values and are on screen
// everything else is either unchanged or will get a complete cell refresh
for itemIdentifier in updatedItemIdentifiers {
guard let indexPath = self.dataSource.indexPath(for: itemIdentifier) else { continue }
guard let cell = self.collectionView.cellForItem(at: indexPath) else { continue }
self.cellUpdater(self.collectionView, cell, indexPath, itemIdentifier.value)
}
}
if animatingDifferences {
UIView.animate(withDuration: 0.2, animations: updates)
} else {
updates()
}
self.dataSource.apply(converted, animatingDifferences: animatingDifferences)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment