A descriptive, diffable data source for UICollectionView.
The motivation for the ListDiffUI framework is to hide the tedious details of playing with the indexPaths and managing consistence between data and views. Vanilla use of UICollectionView/UICollectionViewDataSource is error prone as developers need to be extra cautious with informing UICollectionView of data source changes, handling indexPaths, managing cell reusal, etc. The complexicity grows exponentially if cells in the UICollectionView can be heterogeneous.
ListDiffUI draws inspirations from SwiftUI, UICollectionViewDiffableDataSource, IGListKit and frameworks from other platforms (e.g. React). It provides developers an paradigm of managing each cell in a MVVMC fashion, and a descriptive interface to declare a potentially heterogeneous data source for UICollectionView.
Features
MVVMC Architechture
ListDiffUI employs Model-View-ViewModel-Controller architechture for cells in the list.
Each type of cell is defined by a ViewModel:
public protocol ListViewModel: Identifiable
ViewModels in ListDiffUI framework are expected to be lightweight (immutable) structs that are derived from the underlying data models. They provide interface for identifing cells and equality check.
A ViewState:
public protocol ListViewState
ViewStates in ListDiffUI framework are expected to be lightweight structs as well. ViewState should contain fields that affects the appearance of cells, but are not derived from data models. For example, a flag to represent whether a cell is in expanded or collapsed.
A Cell:
open class ListCell: UICollectionViewCell
Cell is a subclass of UICollectionViewCell with several additions and overrides to make it work with CellControllers in the framework.
A CellController:
open class ListCellController<
ListViewModelType: ListViewModel & Equatable,
ListViewStateType: ListViewState,
ListCellType: ListCell
>: AnyListCellController
CellController is expected to be the place for business logic. At the bare minimum, CellController should provide the size of the cell, and be responsible for configuring the cell based on ViewModel and ViewState. Note that during the lifecycle of a ListDiffDataSource, Cell may be reused just like UICollectionViewCell, but CellControllers are never reused, making it the perfect place to persist ViewState and other data.
Uni-directional Dataflow
Data flows in one direction in ListDiffUI. Any data mutation logic should update model (not managed by the ListDiffUI framework) first, and then update ViewModel. This greatly reduces potentional data inconsitency (and crashes) between model and view.
The above diagram provides a more comprehensive look at how data flows in ListDiffUI framework.
ListDiffUI works well with any Reactive framework such as Combine or RxSwift, where developers can observe model changes and update ListDiffDataSource.
Descriptive
Describe the structure of the list, with sections:
Section provides an intuitive interface for developers to describe how the UICollectionView should look like, that supports heterogeneity by design.
Diff updates
ListDiffUI internally uses the ListDiff algorithm to compute diff and perform batch updates on the collection view.
Both identity and equality are provided through ViewModel interface. Identical and equal ViewModel means no update to an existing cell, whereas identical but not equal ViewModel will trigger an update of the existing cell.
Limitations
Currently ListDiffUI requires UICollectionView to use UICollectionViewFlowLayout (or its subclasses) as it relies on collectionView(_:layout:sizeForItemAt:) method of UICollectionViewDelegateFlowLayout protocol to provide size of cells.
Although ListDiffUI’s section interface provides a way to declare the structure of the list with potentially multiple sections or even nested sections, it gets mapped to a single UICollectionView section internally. As a result it does not support supplemental views for multiple sections.
As ListDiffUI framework hides details of managing indexPaths explicitly, it is not as straightforward if one wants to use an indexPath related API on the UICollectionView. For example, indexPath(for:), cellForItem(at:), scrollToItem(at:at:animated:).
Quick Start Guide
Assuming we are building a ToDo list, to build it with ListDiffUI framework:
Build cell with MVVMC architecture
Start by defining the ViewModel and ViewState for a ToDo list cell:
```swift
struct ToDoItemViewModel: ListViewModel, Equatable {
var identifier: String
var description: String
}
struct ToDoItemViewState: ListViewState {
var completed = false
}
Note that here completed is on ViewState. If it is part of the data model (e.g., it is persisted across sessions), it should be moved to ViewModel instead.
- Implement cell:
```swift
final class ToDoItemCell: ListCell {
var descriptionLabel: UILabel
var completedButton: UIButton
...
}
This is usually the same as how one would do it with vanilla UICollectionViewCell.
Note that in didMount we are removing all targets on the button first to account for cell reuse. ListDiffUI framework does not dictate how cell communicates with controller to handle user actions. The above example is one way. One may also use delegate pattern, and set controller to be the delegate of the cell in didMount.
Create ListDiffDataSource
let dataSource = ListDiffDataSource(collectionView: collectionView)
Observe data model updates, and set root section on the ListDiffDataSource
In BUILD file, add @ListDiffUI//:ListDiffUI to your library’s deps.
Copy source code over
It’s MIT license.
Comparison with similar frameworks
SwiftUI
If SwiftUI is an option that works for your case, there’s no reason to go back to using UICollectionView or ListDiffUI framework.
UICollectionViewDiffableDataSource
UICollectionViewDiffableDataSource uses snapshots to represent view model and compute diff. It is relatively new and may evolve into a more powerful framework. As of iOS 16, there are two ways to create a snapshot:
Loading the snapshot with identifiers using appendSections and appendItems
Compared to ListDiffUI, this method of creating a snapshot does not provide a descriptive interface. The diffing process does not compute item updates either. User is responsible for computing updates to an existing item.
Populate snapshot with lightweight data structures
Compared to ListDiffUI, this method does not track the identity of items.
Neither of the methods offers something like ListDiffUI’s Section interface that can easily support heterogeneity.
IGListKit
ListDiffUI is quite similar to IGListKit, and uses the same ListDiff algorithm for diffing. ListDiffUI additionally offers:
A descriptive interface to describe the structure of the collection view.
ListDiffUI
A descriptive, diffable data source for UICollectionView.
The motivation for the ListDiffUI framework is to hide the tedious details of playing with the indexPaths and managing consistence between data and views. Vanilla use of UICollectionView/UICollectionViewDataSource is error prone as developers need to be extra cautious with informing UICollectionView of data source changes, handling indexPaths, managing cell reusal, etc. The complexicity grows exponentially if cells in the UICollectionView can be heterogeneous.
ListDiffUI draws inspirations from SwiftUI, UICollectionViewDiffableDataSource, IGListKit and frameworks from other platforms (e.g. React). It provides developers an paradigm of managing each cell in a MVVMC fashion, and a descriptive interface to declare a potentially heterogeneous data source for UICollectionView.
Features
MVVMC Architechture
ListDiffUI employs Model-View-ViewModel-Controller architechture for cells in the list.
Each type of cell is defined by a ViewModel:
ViewModels in ListDiffUI framework are expected to be lightweight (immutable) structs that are derived from the underlying data models. They provide interface for identifing cells and equality check.
ViewStates in ListDiffUI framework are expected to be lightweight structs as well. ViewState should contain fields that affects the appearance of cells, but are not derived from data models. For example, a flag to represent whether a cell is in expanded or collapsed.
A Cell:
Cell is a subclass of UICollectionViewCell with several additions and overrides to make it work with CellControllers in the framework.
A CellController:
CellController is expected to be the place for business logic. At the bare minimum, CellController should provide the size of the cell, and be responsible for configuring the cell based on ViewModel and ViewState. Note that during the lifecycle of a ListDiffDataSource, Cell may be reused just like UICollectionViewCell, but CellControllers are never reused, making it the perfect place to persist ViewState and other data.
Uni-directional Dataflow
Data flows in one direction in ListDiffUI. Any data mutation logic should update model (not managed by the ListDiffUI framework) first, and then update ViewModel. This greatly reduces potentional data inconsitency (and crashes) between model and view.
The above diagram provides a more comprehensive look at how data flows in ListDiffUI framework.
ListDiffUI works well with any Reactive framework such as Combine or RxSwift, where developers can observe model changes and update ListDiffDataSource.
Descriptive
Describe the structure of the list, with sections:
Section provides an intuitive interface for developers to describe how the UICollectionView should look like, that supports heterogeneity by design.
Diff updates
ListDiffUI internally uses the ListDiff algorithm to compute diff and perform batch updates on the collection view.
Both identity and equality are provided through ViewModel interface. Identical and equal ViewModel means no update to an existing cell, whereas identical but not equal ViewModel will trigger an update of the existing cell.
Limitations
Currently ListDiffUI requires UICollectionView to use UICollectionViewFlowLayout (or its subclasses) as it relies on
collectionView(_:layout:sizeForItemAt:)
method of UICollectionViewDelegateFlowLayout protocol to provide size of cells.Although ListDiffUI’s section interface provides a way to declare the structure of the list with potentially multiple sections or even nested sections, it gets mapped to a single UICollectionView section internally. As a result it does not support supplemental views for multiple sections.
As ListDiffUI framework hides details of managing indexPaths explicitly, it is not as straightforward if one wants to use an indexPath related API on the UICollectionView. For example,
indexPath(for:)
,cellForItem(at:)
,scrollToItem(at:at:animated:)
.Quick Start Guide
Assuming we are building a ToDo list, to build it with ListDiffUI framework:
Build cell with MVVMC architecture
struct ToDoItemViewState: ListViewState { var completed = false }
This is usually the same as how one would do it with vanilla UICollectionViewCell.
Implement controller logic:
Note that in didMount we are removing all targets on the button first to account for cell reuse. ListDiffUI framework does not dictate how cell communicates with controller to handle user actions. The above example is one way. One may also use delegate pattern, and set controller to be the delegate of the cell in didMount.
Create ListDiffDataSource
Observe data model updates, and set root section on the ListDiffDataSource
And that’s it, ListDiffUI framework will take care of building the root section into an array of view models and updating UI accordingly.
Refer to the sample apps for some examples, that showcases a few additional features in the ListDiffUI framework, including:
Installation
Via Swift Package Manager
https://swiftpackageindex.com/siyuyue/ListDiffUI
Via bazel
In WORKSPACE file:
In BUILD file, add
@ListDiffUI//:ListDiffUI
to your library’s deps.Copy source code over
It’s MIT license.
Comparison with similar frameworks
SwiftUI
If SwiftUI is an option that works for your case, there’s no reason to go back to using UICollectionView or ListDiffUI framework.
UICollectionViewDiffableDataSource
UICollectionViewDiffableDataSource uses snapshots to represent view model and compute diff. It is relatively new and may evolve into a more powerful framework. As of iOS 16, there are two ways to create a snapshot:
Loading the snapshot with identifiers using
appendSections
andappendItems
Compared to ListDiffUI, this method of creating a snapshot does not provide a descriptive interface. The diffing process does not compute item updates either. User is responsible for computing updates to an existing item.
Populate snapshot with lightweight data structures
Compared to ListDiffUI, this method does not track the identity of items.
Neither of the methods offers something like ListDiffUI’s Section interface that can easily support heterogeneity.
IGListKit
ListDiffUI is quite similar to IGListKit, and uses the same ListDiff algorithm for diffing. ListDiffUI additionally offers: