ListDiffUI

UICollectionView 的描述性、可差异数据源。

ListDiffUI 框架的动机是隐藏操作 indexPaths 和管理数据与视图之间一致性的繁琐细节。原生使用 UICollectionView/UICollectionViewDataSource 容易出错,因为开发者需要格外小心地通知 UICollectionView 数据源更改、处理 indexPaths、管理 cell 重用等。如果 UICollectionView 中的 cell 可以是异构的,复杂性会呈指数级增长。

ListDiffUI 从 SwiftUI、UICollectionViewDiffableDataSource、IGListKit 以及其他平台(例如 React)的框架中汲取灵感。它为开发者提供了一种以 MVVMC 方式管理每个 cell 的范例,以及一个描述性接口来声明 UICollectionView 的潜在异构数据源。

特性

MVVMC 架构

ListDiffUI 对列表中的 cell 采用 Model-View-ViewModel-Controller 架构。

单向数据流

数据在 ListDiffUI 中单向流动。任何数据突变逻辑都应首先更新模型(不由 ListDiffUI 框架管理),然后更新 ViewModel。这大大减少了模型和视图之间潜在的数据不一致(和崩溃)。

Untitled Diagram

上面的图表更全面地展示了数据在 ListDiffUI 框架中的流动方式。

ListDiffUI 可以很好地与任何 Reactive 框架(如 Combine 或 RxSwift)配合使用,开发者可以观察模型更改并更新 ListDiffDataSource。

描述性

描述列表的结构,包含 sections

dataSource.setRootSection(
  CompositeSection(
    ListSection<
      Bool, LoadingSpinnerController
    >(isLoading) {
      $0 ? LoadingSpinnerViewModel() : nil
    },
    ListSection<
      ItemViewModel, ItemCellController
    >(items)
  )
)

Section 为开发者提供了一个直观的界面来描述 UICollectionView 的外观,该界面在设计上支持异构性。

Diff 更新

ListDiffUI 内部使用 ListDiff 算法来计算 diff 并在 collection view 上执行批量更新。

身份和相等性都通过 ViewModel 接口提供。相同且相等的 ViewModel 意味着不对现有 cell 进行更新,而相同但不相等的 ViewModel 将触发对现有 cell 的更新。

局限性

快速入门指南

假设我们正在构建一个 ToDo 列表,要使用 ListDiffUI 框架构建它

  1. 使用 MVVMC 架构构建 cell

    • 首先为 ToDo 列表 cell 定义 ViewModel 和 ViewState
    struct ToDoItemViewModel: ListViewModel, Equatable {
      var identifier: String
      var description: String
    }
    
    struct ToDoItemViewState: ListViewState {
      var completed = false
    }

    请注意,这里的 completed 位于 ViewState 上。如果它是数据模型的一部分(例如,它在会话之间持久存在),则应将其移动到 ViewModel 中。

    • 实现 cell
    final class ToDoItemCell: ListCell {
    
      var descriptionLabel: UILabel
      var completedButton: UIButton
    
      ...
    }

    这通常与使用原生 UICollectionViewCell 的方式相同。

    • 实现 controller 逻辑
    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 的委托。

  2. 创建 ListDiffDataSource

    let dataSource = ListDiffDataSource(collectionView: collectionView)
  3. 观察数据模型更新,并在 ListDiffDataSource 上设置根 section

    dataSource.setRootSection(
      ListSection<
        ToDoItem, ToDoItemCellController
      >(items) {
        ToDoItemViewModel(identifier: $0.id, description: $0.description)
      }
    )

就这样,ListDiffUI 框架将负责将根 section 构建为 view model 数组并相应地更新 UI。

有关一些示例,请参阅示例应用程序,其中展示了 ListDiffUI 框架中的一些附加功能,包括

安装

通过 Swift Package Manager

https://swiftpackageindex.cn/siyuyue/ListDiffUI

通过 bazel

在 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

如果 SwiftUI 是适用于你的情况的选项,则没有理由再回头使用 UICollectionView 或 ListDiffUI 框架。

UICollectionViewDiffableDataSource

UICollectionViewDiffableDataSource 使用快照来表示 view model 并计算 diff。它相对较新,可能会发展成为更强大的框架。截至 iOS 16,有两种创建快照的方法

  1. 使用 appendSectionsappendItems 通过标识符加载快照

    与 ListDiffUI 相比,这种创建快照的方法不提供描述性接口。diffing 过程也不计算 item 更新。用户负责计算现有 item 的更新。

  2. 使用轻量级数据结构填充快照

    与 ListDiffUI 相比,此方法不跟踪 item 的身份。

这两种方法都没有提供像 ListDiffUI 的 Section 接口这样的东西,它可以轻松支持异构性。

IGListKit

ListDiffUI 与 IGListKit 非常相似,并使用相同的 ListDiff 算法进行 diffing。ListDiffUI 额外提供