UICollectionView 的描述性、可差异数据源。
ListDiffUI 框架的动机是隐藏操作 indexPaths 和管理数据与视图之间一致性的繁琐细节。原生使用 UICollectionView/UICollectionViewDataSource 容易出错,因为开发者需要格外小心地通知 UICollectionView 数据源更改、处理 indexPaths、管理 cell 重用等。如果 UICollectionView 中的 cell 可以是异构的,复杂性会呈指数级增长。
ListDiffUI 从 SwiftUI、UICollectionViewDiffableDataSource、IGListKit 以及其他平台(例如 React)的框架中汲取灵感。它为开发者提供了一种以 MVVMC 方式管理每个 cell 的范例,以及一个描述性接口来声明 UICollectionView 的潜在异构数据源。
ListDiffUI 对列表中的 cell 采用 Model-View-ViewModel-Controller 架构。
每种类型的 cell 都由一个 ViewModel 定义
public protocol ListViewModel: Identifiable
ListDiffUI 框架中的 ViewModels 预计是轻量级(不可变)的结构体,它们从底层数据模型派生而来。它们为识别 cell 和相等性检查提供接口。
public protocol ListViewState
ListDiffUI 框架中的 ViewStates 也预计是轻量级结构体。ViewState 应包含影响 cell 外观但不从数据模型派生的字段。例如,表示 cell 是展开还是折叠状态的标志。
Cell
open class ListCell: UICollectionViewCell
Cell 是 UICollectionViewCell 的子类,添加和重写了一些内容,使其可以与框架中的 CellControllers 一起工作。
CellController
open class ListCellController<
ListViewModelType: ListViewModel & Equatable,
ListViewStateType: ListViewState,
ListCellType: ListCell
>: AnyListCellController
CellController 预计是放置业务逻辑的地方。最基本的要求是,CellController 应提供 cell 的大小,并负责基于 ViewModel 和 ViewState 配置 cell。请注意,在 ListDiffDataSource 的生命周期中,Cell 可能会像 UICollectionViewCell 一样被重用,但 CellControllers 永远不会被重用,这使其成为持久化 ViewState 和其他数据的理想场所。
数据在 ListDiffUI 中单向流动。任何数据突变逻辑都应首先更新模型(不由 ListDiffUI 框架管理),然后更新 ViewModel。这大大减少了模型和视图之间潜在的数据不一致(和崩溃)。
上面的图表更全面地展示了数据在 ListDiffUI 框架中的流动方式。
ListDiffUI 可以很好地与任何 Reactive 框架(如 Combine 或 RxSwift)配合使用,开发者可以观察模型更改并更新 ListDiffDataSource。
描述列表的结构,包含 sections
dataSource.setRootSection(
CompositeSection(
ListSection<
Bool, LoadingSpinnerController
>(isLoading) {
$0 ? LoadingSpinnerViewModel() : nil
},
ListSection<
ItemViewModel, ItemCellController
>(items)
)
)
Section 为开发者提供了一个直观的界面来描述 UICollectionView 的外观,该界面在设计上支持异构性。
ListDiffUI 内部使用 ListDiff 算法来计算 diff 并在 collection view 上执行批量更新。
身份和相等性都通过 ViewModel 接口提供。相同且相等的 ViewModel 意味着不对现有 cell 进行更新,而相同但不相等的 ViewModel 将触发对现有 cell 的更新。
目前,ListDiffUI 要求 UICollectionView 使用 UICollectionViewFlowLayout(或其子类),因为它依赖于 UICollectionViewDelegateFlowLayout 协议的 collectionView(_:layout:sizeForItemAt:)
方法来提供 cell 的大小。
尽管 ListDiffUI 的 section 接口提供了一种声明列表结构的方式,该结构可能包含多个 section 甚至嵌套 section,但它在内部被映射到单个 UICollectionView section。因此,它不支持多个 section 的补充视图。
由于 ListDiffUI 框架隐藏了显式管理 indexPaths 的细节,因此如果想要在 UICollectionView 上使用与 indexPath 相关的 API,则不是那么直接。例如,indexPath(for:)
、cellForItem(at:)
、scrollToItem(at:at:animated:)
。
假设我们正在构建一个 ToDo 列表,要使用 ListDiffUI 框架构建它
使用 MVVMC 架构构建 cell
struct ToDoItemViewModel: ListViewModel, Equatable {
var identifier: String
var description: String
}
struct ToDoItemViewState: ListViewState {
var completed = false
}
请注意,这里的 completed 位于 ViewState 上。如果它是数据模型的一部分(例如,它在会话之间持久存在),则应将其移动到 ViewModel 中。
final class ToDoItemCell: ListCell {
var descriptionLabel: UILabel
var completedButton: UIButton
...
}
这通常与使用原生 UICollectionViewCell 的方式相同。
final class ToDoItemCellController: ListCellController<
ToDoItemViewModel,
ToDoItemViewState,
ToDoItemCell
> {
override func itemSize(containerSize: CGSize) -> CGSize {
return CGSize(width: containerSize.width, height: 40)
}
override func configureCell(cell: LabelCell) {
cell.descriptionLabel.text = viewModel.description
cell.completedButton.isSelected = viewState.completed
}
override func didMount(onCell cell: LabelCell) {
cell.completedButton.removeTarget(nil, action: nil, for: .touchUpInside)
cell.completedButton.addTarget(self, action: #selector(didTapComplete), for: .touchUpInside)
}
@objc
private func didTapComplete() {
var state = viewState
state.completed = !state.completed
updateState(state)
}
}
请注意,在 didMount 中,我们首先删除按钮上的所有 targets,以考虑 cell 重用。ListDiffUI 框架不规定 cell 如何与 controller 通信以处理用户操作。上面的示例是一种方式。也可以使用委托模式,并将 controller 设置为 didMount 中 cell 的委托。
创建 ListDiffDataSource
let dataSource = ListDiffDataSource(collectionView: collectionView)
观察数据模型更新,并在 ListDiffDataSource 上设置根 section
dataSource.setRootSection(
ListSection<
ToDoItem, ToDoItemCellController
>(items) {
ToDoItemViewModel(identifier: $0.id, description: $0.description)
}
)
就这样,ListDiffUI 框架将负责将根 section 构建为 view model 数组并相应地更新 UI。
有关一些示例,请参阅示例应用程序,其中展示了 ListDiffUI 框架中的一些附加功能,包括
https://swiftpackageindex.cn/siyuyue/ListDiffUI
在 WORKSPACE 文件中
git_repository(
name = "ListDiffUI",
remote = "https://github.com/siyuyue/ListDiffUI.git",
commit = "2097758b9b0bcedabdc0d7916c4d7613f8f0e2b7",
shallow_since = "1671574341 -0800",
)
load(
"@ListDiffUI//:repositories.bzl",
"listdiffui_dependencies",
)
listdiffui_dependencies()
在 BUILD 文件中,将 @ListDiffUI//:ListDiffUI
添加到你的库的 deps 中。
它是 MIT 许可证。
如果 SwiftUI 是适用于你的情况的选项,则没有理由再回头使用 UICollectionView 或 ListDiffUI 框架。
UICollectionViewDiffableDataSource 使用快照来表示 view model 并计算 diff。它相对较新,可能会发展成为更强大的框架。截至 iOS 16,有两种创建快照的方法
使用 appendSections
和 appendItems
通过标识符加载快照
与 ListDiffUI 相比,这种创建快照的方法不提供描述性接口。diffing 过程也不计算 item 更新。用户负责计算现有 item 的更新。
使用轻量级数据结构填充快照
与 ListDiffUI 相比,此方法不跟踪 item 的身份。
这两种方法都没有提供像 ListDiffUI 的 Section 接口这样的东西,它可以轻松支持异构性。
ListDiffUI 与 IGListKit 非常相似,并使用相同的 ListDiff 算法进行 diffing。ListDiffUI 额外提供