ReactiveCollectionsKit CI

适用于 iOS 的数据驱动、声明式、响应式、可差异化集合(和列表!)。一个现代、快速、灵活的 UICollectionView 库,让集合视图的使用更加便捷。

关于

这个库是我从构建和维护 IGListKitReactiveListsJSQDataSourcesKit 中学到的一切的结晶。事不过三,这次是第四次!🍀

这个库包含了对上述库的许多改进、优化和提炼。我融合了我认为这些库中最好的想法和架构设计元素,同时消除了或改进了它们的缺点。重要的是,这个库使用了现代的 UICollectionView API — 即 UICollectionViewDiffableDataSourceUICollectionViewCompositionalLayout,这两个 API 在编写之前的库时都不可用。这个库没有第三方依赖,并且是用 Swift 编写的。

SwiftUI 怎么样?

SwiftUI 的性能仍然是一个重要问题,更不用说所有的错误、缺失的 API 以及缺乏向旧操作系统版本的回溯 API 了。SwiftUI 仍然没有提供一个合适的 UICollectionView 替代品。是的,Grid 存在,但它远不能替代 UICollectionViewUICollectionViewLayout。虽然 SwiftUIList 在大多数情况下都很好,但当您有大量数据时,LazyVStackLazyHStack 都存在严重的性能问题。

主要特性

主要特性
🏛️ 具有可重用组件的声明式、数据驱动架构
🔐 不可变、单向数据流
🔀 通过 Swift 6 严格的并发检查,防止数据竞争
🤖 自动对单元格、节和补充视图进行差异化比较
🎟️ 自动注册和出列单元格和补充视图
📐 自动调整单元格和补充视图的大小
🔠 创建具有混合数据类型的集合,由协议和泛型提供支持
🔎 对模型的差异化行为进行细粒度控制
🚀 通过协议扩展提供合理的默认值
🛠️ 可扩展的 API,可通过协议自定义
📱 核心是简单的 UICollectionViewUICollectionViewDiffableDataSource
🙅 永远不要再调用 apply(_ snapshot:)reloadData()performBatchUpdates()
🙅 永远不要再调用 register(_:forCellWithReuseIdentifier:)dequeueReusableCell(withReuseIdentifier:for:)
🙅 永远不要再实现 DataSourceDelegate 方法
🏎️ 全部用 Swift 编写,零第三方依赖
完全单元测试

值得注意的是,这个库合并并专注于 UICollectionView。**没有** UITableView 支持,因为 UICollectionView 现在有一个 列表布局,完全消除了对 UITableView 的需求。

开始使用

提示

查看此仓库中包含的广泛的 示例项目

这是一个从数据模型数组构建简单静态列表的简短示例。

let models = [/* array of some data models */]

// create cell view models from the data models
let cellViewModels = models.map {
    MyCellViewModel($0)
}

// create the sections with cells
let section = SectionViewModel(id: "my_section", cells: cellViewModels)

// create the collection with sections
let collectionViewModel = CollectionViewModel(sections: [section])

// initialize the driver with the view model and other components
let driver = CollectionViewDriver(
    view: collectionView,
    viewModel: collectionViewModel,
    emptyViewProvider: provider,
    cellEventCoordinator: coordinator
)

// the collection view is updated and animated automatically

// when the models change, generate a new view model (like above)
let updated = CollectionViewModel(sections: [/* updated items and sections */])
driver.update(viewModel: updated)

重要提示

使用此库时,应避免调用以下 UICollectionView API

要求

安装

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/jessesquires/ReactiveCollectionsKit.git", from: "0.1.0")
]

或者,您可以 直接通过 Xcode 添加包。

文档

您可以在 此处 阅读文档。使用 jazzy 生成。由 GitHub Pages 托管。

文档也可以在 Swift Package Index 上找到。

关于库架构的说明

以下是一些关于此库中的架构和核心概念的高级说明,以及与我参与过的其他库(IGListKitReactiveListsJSQDataSourcesKit)的比较。

概述

IGListKit 的主要缺点是 Objective-C 类型系统中缺乏表现力、一些样板设置、可变性和使用节作为基本/根本组件。虽然它是通用的,但大部分设计都是根据我们在 Instagram 的具体需求而定的。IGListKit 的正确之处在于差异化 — 事实上,我们开创了整个想法。UIKit 中的 API *在我们发布 IGListKit 之后*才出现,并且深受我们所做的事情的影响。

ReactiveLists 的主要缺点是它使用旧的 UIKit API 和自定义的第三方差异化库。它为表格和集合维护完全独立的基础设施,这重复了很多功能。有一个 TableViewModel 和一个 CollectionViewModel 等,用于 UITableViewUICollectionView。它也有点不完整,因为我们只实现了我们在 PlanGrid 需要的东西。它早于用于差异化和列表布局的现代集合视图 API。ReactiveLists 的正确之处在于声明式 API,使用单元格作为基本/根本组件,以及单向数据流。

JSQDataSourcesKit 在某种意义上来说一直有点实验性和学术性。它不做任何差异化,并且也有独立的表格和集合基础设施,因为它早于那些现代集合视图 API。它主要关注于构建类型安全的数据源,消除与 UITableViewDataSourceUICollectionViewDataSource 相关的样板代码。最终,泛型过于笨拙。有关更多详细信息,请参阅我的文章弃用 JSQDataSourcesKitJSQDataSourcesKit 的正确之处在于使用泛型提供类型安全性的想法,尽管它没有得到很好的执行。

所有这些经验和知识最终促使我编写了这个库 ReactiveCollectionsKit,它的目标是保留上面库中的所有好想法和设计,同时解决它们的缺点。我编写或维护了所有这些库,所以希望这次能做对!:)

不可变性和单向数据流

ReactiveLists 的正确之处在于不可变性、声明式 API 和单向数据流。使用 ReactiveLists,您可以声明式地定义整个集合视图模型,并在底层数据模型发生更改时重新生成它。

同时,IGListKit 非常命令式和可变的。使用 IGListKit,在您连接 IGListAdapterIGListSectionController 对象后,您可以就地更新节。IGListKit 鼓励不可变的数据模型,但这在 Objective-C 中是不可执行的,API 中也没有强制执行。IGListKit 在某种意义上确实具有单向数据流,但您通过 IGListAdapterDataSource 以命令式的方式提供您的数据,这还需要您手动管理数据模型对象与其对应的 IGListSectionController 对象之间的映射。

ReactiveCollectionsKit 改进了 ReactiveListsIGListKit 采用的方法,并删除或整合了 IGListKit 所需的样板代码。

CellViewModel 是库中的基本或“原子”组件。它封装了单个单元格的所有数据、配置、交互和注册。这类似于 ReactiveLists。在 IGListKit 中,此组件对应于 IGListSectionControllerIGListKit 的一个缺点是“原子”组件是多个项目的整个节 — 一个节可能只有一个项目,在这种情况下,它更像 CellViewModel

CollectionViewModel 定义了集合的整个结构。它是数据模型集合的不可变表示,可以是任何东西。“driver” 术语借用自 ReactiveLists。此组件或多或少等同于 IGListKit 中的 IGListAdapter

这两个核心组件共同实现了单向数据流。一般工作流程是:(1)获取或更新您的数据模型,(2)从该数据生成您的 CellViewModel 对象并完成 CollectionViewModel,(3)在 CollectionViewDriver 上设置视图模型,然后它将使用先前设置的模型执行差异化并更新 UICollectionView

差异化:标识和相等性

理解差异化需要理解两个核心概念:**标识** 和 **相等性**。在 ReactiveCollectionsKit 中,这些概念由 DiffableViewModel 建模。

typealias UniqueIdentifier = AnyHashable

protocol DiffableViewModel: Identifiable, Hashable {
    var id: UniqueIdentifier { get }
}

**标识** 关注于**永久地** 和 **唯一地** 识别一个对象的单个实例。标识*永远不会*改变。标识回答了问题“这是谁?” 例如,护照封装了一个人的身份概念。护照永久且唯一地识别并对应于一个人。标识由 Identifiable 协议和相应的 id 属性捕获。

相等性 关注单个唯一对象的短暂特性属性,这些特性或属性会随着时间的推移而改变。 相等性回答了这样一个问题:"在这些具有相同 id 的对象中,哪一个是最新版本的?" 例如,一个人是一个独特的实体,但是他们可以改变他们的发型,他们可以穿不同的衣服,并且通常可以改变他们外貌的任何方面。 虽然我们可以随时使用他们的护照来唯一识别一个人,但他们的外貌会日复一日或年复一年地发生变化。 相等性由 Hashable (和 Equatable) 协议以及相应的 ==hash(into:) 函数捕获。

使用这个例子,考虑构建一个要在集合中显示的人员列表。 我们可以唯一地识别集合中的每个人(使用 id)。 这使我们能够确定 (1) 他们是否存在,(2) 他们的确切位置,(3) 他们是否已被删除/移动/添加。 接下来,我们可以确定自从我们上次看到他们以来他们是否发生了变化(使用 ==)。 这使我们能够确定集合中唯一的某个人何时需要重新加载或刷新。

IGListKitReactiveLists 都正确地实现了这一点,但它们的实现更加繁琐和手动。 ReactiveCollectionsKit 通过上面的 DiffableViewModel 协议(以及 Swift 的类型系统)改进了这两种实现。 标识符可以是任何可哈希的,但通常这只是一个 String。 因为 Swift 可以自动合成对 Hashable 的一致性,所以大多数客户端将免费获得所有这些功能。 如果你需要优化你的 Hashable 实现,你可以手动实现该协议

func hash(into hasher: inout Hasher)

static func == (left: Self, right: Self) -> Bool

重要提示

UIKit 中的集合视图 API 不处理相等性UICollectionViewDiffableDataSource 只关注标识 — 它为你处理结构(插入/删除/移动),但是你必须自己处理项目属性更改的重新加载(或重新配置)。 (参见:Tyler Fox。)

这是该库的主要动机之一,也是需要类似库的原因。 当使用 UICollectionViewDiffableDataSource 时,你必须自己跟踪集合中所有项目的属性更改,然后相应地重新加载/重新配置。

CellViewModel 协议

如上所述,CellViewModel 是该库的“基本”或“原子”组件。 这就是“奇迹发生的地方”。 CellViewModel 声明性地定义了单元格显示、差异化和交互所需的一切。 它应该封装配置单元格和处理交互事件所需的所有数据。 该模型还包括如何向集合视图注册单元格以供重用的声明性定义。

CellViewModel 协议继承自 DiffableViewModelViewRegistrationProvider 以完成这些任务。 这允许自动和可定制的差异化以及自动视图注册。 由于 Swift 通过协议扩展的默认实现,你可以免费获得很多默认行为。 如上所述,你可以通过编译器合成的定义免费获得 EquatableHashable 的一致性。 对于 ViewRegistrationProvider,你可以免费获得基于类的默认注册。 此功能类似于 ReactiveListsIGListKit 也提供自动注册,但它非常隐式。

本质上,来自集合视图的所有数据源和委托方法都被转发到 CellViewModel 的每个实例。

对于标题、页脚和补充视图,有一个类似的 SupplementaryViewModel

泛型和类型擦除

IGListKit 尽管有一些 Swift 改进,但仍然受到 Objective-C 类型系统中缺乏表现力的影响。 ReactiveLists 在这方面做得更好,但它的出现早于 Swift 泛型和存在类型的现代改进。 在 ReactiveLists 中,配置单元格需要将 UITableViewCellUICollectionViewCell 强制转换为视图模型的特定单元格类型。 在 ReactiveCollectionsKit 中,这可以通过泛型和关联类型来解决。

protocol CellViewModel: DiffableViewModel, ViewRegistrationProvider {
    associatedtype CellType: UICollectionViewCell

    func configure(cell: CellType)

    // other members...
}

使用泛型是 JSQDataSourcesKit 做对的事情,某种程度上。 虽然在 JSQDataSourcesKit 中避免转换视图类型很好,但泛型一直扩展到数据源层,这导致了糟糕的 API 人体工程学以及在显示混合数据类型方面的极端困难。 你也不能混合补充视图类型。 你可以使用 enum 来解决单元格的限制,但这不是很实用。

为了缓解 JSQDataSourcesKit 中遇到的这些缺点,并在构建 section 时处理下游的异构类型(通过 SectionViewModel),你必须擦除单元格类型。 此功能通过 CellViewModel 上的扩展方法提供。

func eraseToAnyViewModel() -> AnyCellViewModel

SupplementaryViewModel 遵循类似的设计,允许你混合补充视图的类型。

在实践中,这意味着当使用混合数据类型时,你最终需要将你的特定单元格视图模型转换为 AnyCellViewModel

let people = [Person]()

let cellViewModels = people.map {
    PersonCellViewModel($0).eraseToAnyViewModel()
}

但是,由于 Swift,你会注意到 SectionViewModel 提供了许多使用泛型的便捷初始化器。 在你没有混合数据类型的情况下,泛型初始化器允许你忽略这个实现细节并为你处理类型擦除。

其他资源

贡献

有兴趣为这个项目做出贡献吗? 请查看下面的指南。

此外,请考虑赞助这个项目购买我的应用程序! ✌️

鸣谢

Jesse Squires 创建和维护。

许可证

在 MIT 许可证下发布。 有关详细信息,请参阅 LICENSE

版权所有 © 2019-present Jesse Squires。