Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Sources/FunctionalTableData/AnyHashableConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public struct AnyHashableConfig: Hashable, HashableCellConfigType {
}
}

init(_ base: CellConfigType, itemPath: ItemPath) {
self.base = base
self.hashable = AnyHashable(itemPath)
}

public init(_ base: HashableCellConfigType) {
self.base = base
self.hashable = base.hashable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ struct AnyCollectionSection: Hashable {
private var impl: CollectionSection

public var key: String { impl.key }
public var items: [HashableCellConfigType]
public var items: [AnyHashableConfig]
public var supplementaries: [CollectionSupplementaryItemConfig] {
get { impl.supplementaries }
set { impl.supplementaries = newValue }
}

public init(_ section: CollectionSection) {
items = section.items.map { AnyHashableConfig($0, sectionKey: section.key) }
items = section.items.map { AnyHashableConfig($0, itemPath: ItemPath(sectionKey: section.key, itemKey: $0.key)) }
impl = section
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ final class DiffableDataSourceFunctionalCollectionDataDiffer: FunctionalCollecti
guard let collectionView = collectionView else { return }
let dataSource = UICollectionViewDiffableDataSource<AnyCollectionSection, AnyHashableConfig>(collectionView: collectionView) { collectionView, indexPath, cellConfig in
let section = self.data.sections[indexPath.section]
guard let cellConfig = section.items.first(where: { $0.key == cellConfig.key }) else { return nil }
let cell = cellConfig.dequeueCell(from: collectionView, at: indexPath)
let accessibilityIdentifier = ItemPath(sectionKey: section.key, itemKey: cellConfig.key).description
cellConfig.accessibility.with(defaultIdentifier: accessibilityIdentifier).apply(to: cell)
Expand Down Expand Up @@ -63,18 +64,23 @@ final class DiffableDataSourceFunctionalCollectionDataDiffer: FunctionalCollecti
let changeSet = CollectionSectionChangeSet(old: oldSections, new: localSections, visibleIndexPaths: indexPaths)
data.sections = localSections

var snapshot = NSDiffableDataSourceSnapshot<AnyCollectionSection, AnyHashableConfig>()
let sections = localSections.map { AnyCollectionSection($0) }
snapshot.appendSections(sections)
for newSection in sections {
snapshot.appendItems(newSection.items.map { AnyHashableConfig($0) }, toSection: newSection)
}
var isFirstRender: Bool = false
if let snapshot = dataSource?.snapshot(), snapshot.numberOfSections == 0 {
isFirstRender = true
}
let shouldAnimate = animated && !isFirstRender
NSException.catchAndHandle {
var snapshot = NSDiffableDataSourceSnapshot<AnyCollectionSection, AnyHashableConfig>()
let sections = localSections.map { AnyCollectionSection($0) }
snapshot.appendSections(sections)
let oldItems = Dictionary(uniqueKeysWithValues: (self.dataSource?.snapshot().itemIdentifiers ?? []).map { ($0, $0) })
for newSection in sections {
snapshot.appendItems(newSection.items, toSection: newSection)
snapshot.reloadItems(newSection.items.filter { item in
guard let oldItem = oldItems[item] else { return false }
return !item.isEqual(oldItem) || item.style != oldItem.style
})
}
self.dataSource?.apply(snapshot, animatingDifferences: shouldAnimate, completion: completion)
} failure: { exception in
if exception.name == NSExceptionName.internalInconsistencyException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,80 @@ import XCTest
@testable import FunctionalTableData

class DiffableDataSourceFunctionalCollectionDataTests: XCTestCase {
func testEqualCellStatesHaveDistinctItemIdentities() {
let (differ, collectionView) = makeDiffer()
let sections = [
SimpleCollectionSection(key: "first", items: [cell("one"), cell("two")]),
SimpleCollectionSection(key: "second", items: [cell("one")])
]

render(sections, with: differ)

let snapshot = differ.dataSource.snapshot()
XCTAssertEqual(snapshot.numberOfItems, 3)
XCTAssertEqual(Set(snapshot.itemIdentifiers.map { $0.hashable }), Set([
AnyHashable(ItemPath(sectionKey: "first", itemKey: "one")),
AnyHashable(ItemPath(sectionKey: "first", itemKey: "two")),
AnyHashable(ItemPath(sectionKey: "second", itemKey: "one"))
]))
XCTAssertEqual(collectionView.numberOfItems(inSection: 0), 2)
XCTAssertEqual(collectionView.numberOfItems(inSection: 1), 1)
}

func testContentChangesReloadWithoutChangingIdentity() {
let (differ, collectionView) = makeDiffer()
render([SimpleCollectionSection(key: "section", items: [cell("one"), cell("two")])], with: differ)
let originalItems = differ.dataSource.snapshot().itemIdentifiers
render([SimpleCollectionSection(key: "section", items: [cell("one", state: "changed"), cell("two")])], with: differ)

let snapshot = differ.dataSource.snapshot()
XCTAssertEqual(snapshot.itemIdentifiers, originalItems)
collectionView.layoutIfNeeded()
let visibleCell = collectionView.cellForItem(at: IndexPath(item: 0, section: 0)) as? CellConfig<DiffableLabelView, String>.CollectionCellType
XCTAssertEqual(visibleCell?.view.text, "changed")
}

func testEqualStateItemsCanMoveBeInsertedAndBeDeleted() {
let (differ, collectionView) = makeDiffer()
render([SimpleCollectionSection(key: "section", items: [cell("one"), cell("two"), cell("three")])], with: differ)
render([SimpleCollectionSection(key: "section", items: [cell("three"), cell("one"), cell("four")])], with: differ)

XCTAssertEqual(differ.dataSource.snapshot().itemIdentifiers.map { $0.key }, ["three", "one", "four"])
XCTAssertEqual(collectionView.numberOfItems(inSection: 0), 3)
}

func testDuplicateKeysAreHandledDuringSnapshotConstruction() {
let invalidSections: [[CollectionSection]] = [
[SimpleCollectionSection(key: "section", items: [cell("one"), cell("one")])],
[
SimpleCollectionSection(key: "section", items: [cell("one")]),
SimpleCollectionSection(key: "section", items: [cell("two")])
]
]
for sections in invalidSections {
let (differ, _) = makeDiffer()
differ.renderAndDiff(sections, animated: false, completion: nil)
XCTAssertEqual(differ.dataSource.snapshot().numberOfItems, 0)
}
}

private func makeDiffer() -> (DiffableDataSourceFunctionalCollectionDataDiffer, UICollectionView) {
let differ = DiffableDataSourceFunctionalCollectionDataDiffer(name: "test", data: CollectionData())
let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: 320, height: 480), collectionViewLayout: UICollectionViewFlowLayout())
differ.collectionView = collectionView
return (differ, collectionView)
}

private func cell(_ key: String, state: String = "same") -> CellConfig<DiffableLabelView, String> {
return CellConfig(key: key, state: state)
}

private func render(_ sections: [CollectionSection], with differ: DiffableDataSourceFunctionalCollectionDataDiffer) {
let completed = expectation(description: "render completed")
differ.renderAndDiff(sections, animated: false) { completed.fulfill() }
waitForExpectations(timeout: 1)
}

func testKeyPathFromRowKey() {
let collectionData = FunctionalCollectionData(name: nil, diffingStrategy: .diffableDataSource)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
Expand Down Expand Up @@ -74,3 +148,11 @@ class DiffableDataSourceFunctionalCollectionDataTests: XCTestCase {
}

}

private final class DiffableLabelView: UILabel, ConfigurableView {
func configure(_ state: String) {
text = state
}

func prepareForReuse() {}
}
Loading