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