A fast, pure-Swift diffable data source for UICollectionView. Drop-in replacement for Apple's UICollectionViewDiffableDataSource with an O(n) Heckel diff algorithm and a high-level declarative API.
Two libraries, one repo:
| Library | Purpose |
|---|---|
| ListKit | Low-level diffing engine and data source. API-compatible with Apple's NSDiffableDataSourceSnapshot. |
| Lists | High-level, ViewModel-driven layer with result-builder DSL, automatic cell registration, pre-built list configurations, and SwiftUI wrappers. |
Documentation — full API reference, guides, and examples.
- iOS 17.0+
- Swift 6.0
- Xcode 17+
dependencies: [
.package(url: "https://github.com/Iron-Ham/ListKit", from: "0.6.1"),
]Then add ListKit, Lists, or both to your target:
.target(name: "MyApp", dependencies: ["ListKit", "Lists"]).external(name: "ListKit"),
.external(name: "Lists"),Define a view model, build a snapshot with the DSL, done:
import Lists
struct ContactItem: CellViewModel, Identifiable {
typealias Cell = UICollectionViewListCell
let id: UUID
let name: String
@MainActor func configure(_ cell: UICollectionViewListCell) {
var content = cell.defaultContentConfiguration()
content.text = name
cell.contentConfiguration = content
}
}
class ContactsViewController: UIViewController {
private var dataSource: ListDataSource<String, ContactItem>!
private var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
let layout = ListLayout.plain()
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
view.addSubview(collectionView)
dataSource = ListDataSource(collectionView: collectionView)
Task {
await dataSource.apply {
SnapshotSection("contacts") {
ContactItem(id: UUID(), name: "Alice")
ContactItem(id: UUID(), name: "Bob")
}
}
}
}
}Lists also provides pre-built configurations (SimpleList, GroupedList, OutlineList) with SwiftUI wrappers, mixed cell type support, and more. See the documentation for the full API.
All benchmarks run in Release configuration with median-of-15 and 5 warmup iterations on Apple Silicon.
| Operation | ListKit | Apple | Speedup |
|---|---|---|---|
| Build 10k items | 0.002 ms | 1.223 ms | 752.7x |
| Build 50k items | 0.006 ms | 6.010 ms | 1,045.3x |
| Build 100 sections x 100 | 0.060 ms | 3.983 ms | 66.4x |
| Delete 5k from 10k | 1.206 ms | 2.448 ms | 2.0x |
| Delete 25/50 sections | 0.180 ms | 1.852 ms | 10.3x |
| Reload 5k items | 0.099 ms | 1.547 ms | 15.7x |
| Query itemIdentifiers 100x | 0.051 ms | 46.364 ms | 908.3x |
Real-world apps store Item.ID in the snapshot, not the full model — the Apple-recommended pattern. These benchmarks use UUID identifiers from an Identifiable model type.
| Operation | ListKit | Apple | Speedup |
|---|---|---|---|
| Build 10k Item.IDs | 0.003 ms | 2.269 ms | 789.2x |
| Delete 5k Item.IDs | 1.358 ms | 4.084 ms | 3.0x |
| Reload 5k Item.IDs | 0.284 ms | 2.738 ms | 9.6x |
ListKit snapshots are pure Swift value types with flat array storage and a lazy reverse index. Apple's NSDiffableDataSourceSnapshot is backed by Objective-C runtime overhead and per-query hashing.
Both libraries implement Paul Heckel's O(n) diff. IGListKit's is Objective-C++; ListKit's is pure Swift. Both sides pre-build their data structures before the timed block so only the diff algorithm is measured — ListDiff() for IGListKit, SectionedDiff.diff() for ListKit. IGListKit diffs a single flat array; ListKit diffs sections first, then items per-section, and reconciles cross-section moves.
| Operation | IGListKit | ListKit | Notes |
|---|---|---|---|
| Diff 10k (50% overlap) | 10.8 ms | 3.9 ms | 2.8x — ListKit wins at common scale |
| Diff 50k (50% overlap) | 55.4 ms | 19.6 ms | 2.8x — ListKit wins at scale too |
| Diff no-change 10k | 9.5 ms | 0.09 ms | 106x — per-section skip makes this free |
| Diff shuffle 10k | 9.8 ms | 3.2 ms | 3.1x — ListKit wins all-moves case |
Per-section diffing skips unchanged sections entirely — the common case for incremental UI updates.
Real-world list updates rarely involve just one type of change. A feed refresh might delete stale items, insert new ones, and re-rank existing items — all at once. These benchmarks combine all three operations in a single diff using Item.ID (UUID) identifiers. "Moderate churn" changes 20% of items per operation type; "heavy churn" changes 50%.
| Scenario | IGListKit | ListKit | Speedup |
|---|---|---|---|
| 10k — moderate churn | 10.6 ms | 3.3 ms | 3.2x |
| 10k — heavy churn | 11.4 ms | 3.9 ms | 2.9x |
| 50k — moderate churn | 52.0 ms | 16.7 ms | 3.1x |
| Scenario | Apple | ListKit | Speedup |
|---|---|---|---|
| 10k — moderate churn | 4.238 ms | 0.005 ms | 782x |
| 10k — heavy churn | 4.359 ms | 0.007 ms | 627x |
| 50k — moderate churn | 22.816 ms | 0.022 ms | 1,049x |
ListKit's full pipeline (snapshot build + diff) for 10k items with moderate churn completes faster than Apple takes to just build the snapshots.
The ultimate end-to-end test: apply() on a real UICollectionViewDiffableDataSource, which includes the diff computation, performBatchUpdates, and all UIKit bookkeeping. Apple's diff algorithm is internal and can't be benchmarked in isolation — this is the only way to measure it.
| Scenario | Apple | ListKit | Speedup |
|---|---|---|---|
| 10k — moderate churn | 11.9 ms | 8.7 ms | 1.4x |
| 10k — heavy churn | 23.5 ms | 14.2 ms | 1.7x |
The speedup is smaller here than in isolated benchmarks because both frameworks share the same UICollectionView overhead (batch updates, layout invalidation). The diff advantage matters most at scale: Apple's heavy-churn apply exceeds the 16 ms frame budget, while ListKit stays under.
ReactiveCollectionsKit wraps Apple's NSDiffableDataSourceSnapshot with type-erased CollectionViewModel layers. These benchmarks compare model construction (building the data structures each library needs before applying to a collection view).
| Operation | ListKit | ReactiveCollectionsKit | Apple | Speedup vs RC |
|---|---|---|---|---|
| Build 10k items | 0.004 ms | 7.5 ms | 1.3 ms | 1,871x |
| Build 50k items | 0.014 ms | 37.4 ms | — | 2,671x |
| Build 100 sections x 100 | 0.058 ms | 7.3 ms | 4.2 ms | 126x |
Run benchmarks with make benchmark.
make setup # install Tuist, fetch dependencies, generate project, install hooks
make build # build ListKit + Lists frameworks
make test # run all tests (ListKit + Lists)
make benchmark # run performance benchmarks
make format # format code with SwiftFormat
make lint # lint with SwiftFormat
make open # open in XcodeMIT