适用于 iOS 的数据驱动、声明式、响应式、可差异化集合(和列表!)。一个现代、快速、灵活的 UICollectionView
库,让集合视图的使用更加便捷。
这个库是我从构建和维护 IGListKit
、ReactiveLists
和 JSQDataSourcesKit
中学到的一切的结晶。事不过三,这次是第四次!🍀
这个库包含了对上述库的许多改进、优化和提炼。我融合了我认为这些库中最好的想法和架构设计元素,同时消除了或改进了它们的缺点。重要的是,这个库使用了现代的 UICollectionView
API — 即 UICollectionViewDiffableDataSource
和 UICollectionViewCompositionalLayout
,这两个 API 在编写之前的库时都不可用。这个库没有第三方依赖,并且是用 Swift 编写的。
SwiftUI
的性能仍然是一个重要问题,更不用说所有的错误、缺失的 API 以及缺乏向旧操作系统版本的回溯 API 了。SwiftUI
仍然没有提供一个合适的 UICollectionView
替代品。是的,Grid
存在,但它远不能替代 UICollectionView
和 UICollectionViewLayout
。虽然 SwiftUI
的 List
在大多数情况下都很好,但当您有大量数据时,LazyVStack
和 LazyHStack
都存在严重的性能问题。
主要特性 | |
---|---|
🏛️ | 具有可重用组件的声明式、数据驱动架构 |
🔐 | 不可变、单向数据流 |
🔀 | 通过 Swift 6 严格的并发检查,防止数据竞争 |
🤖 | 自动对单元格、节和补充视图进行差异化比较 |
🎟️ | 自动注册和出列单元格和补充视图 |
📐 | 自动调整单元格和补充视图的大小 |
🔠 | 创建具有混合数据类型的集合,由协议和泛型提供支持 |
🔎 | 对模型的差异化行为进行细粒度控制 |
🚀 | 通过协议扩展提供合理的默认值 |
🛠️ | 可扩展的 API,可通过协议自定义 |
📱 | 核心是简单的 UICollectionView 和 UICollectionViewDiffableDataSource |
🙅 | 永远不要再调用 apply(_ snapshot:) 、reloadData() 或 performBatchUpdates() |
🙅 | 永远不要再调用 register(_:forCellWithReuseIdentifier:) 或 dequeueReusableCell(withReuseIdentifier:for:) |
🙅 | 永远不要再实现 DataSource 和 Delegate 方法 |
🏎️ | 全部用 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
reloadData()
reconfigureItems(at:)
reloadSections(_:)
reloadItems(at:)
performBatchUpdates(_:completion:)
UICollectionViewDataSource
方法UICollectionViewDelegate
方法dependencies: [
.package(url: "https://github.com/jessesquires/ReactiveCollectionsKit.git", from: "0.1.0")
]
或者,您可以 直接通过 Xcode 添加包。
您可以在 此处 阅读文档。使用 jazzy 生成。由 GitHub Pages 托管。
文档也可以在 Swift Package Index 上找到。
以下是一些关于此库中的架构和核心概念的高级说明,以及与我参与过的其他库(IGListKit
、ReactiveLists
和 JSQDataSourcesKit
)的比较。
IGListKit
的主要缺点是 Objective-C 类型系统中缺乏表现力、一些样板设置、可变性和使用节作为基本/根本组件。虽然它是通用的,但大部分设计都是根据我们在 Instagram 的具体需求而定的。IGListKit
的正确之处在于差异化 — 事实上,我们开创了整个想法。UIKit
中的 API *在我们发布 IGListKit
之后*才出现,并且深受我们所做的事情的影响。
ReactiveLists
的主要缺点是它使用旧的 UIKit
API 和自定义的第三方差异化库。它为表格和集合维护完全独立的基础设施,这重复了很多功能。有一个 TableViewModel
和一个 CollectionViewModel
等,用于 UITableView
和 UICollectionView
。它也有点不完整,因为我们只实现了我们在 PlanGrid 需要的东西。它早于用于差异化和列表布局的现代集合视图 API。ReactiveLists
的正确之处在于声明式 API,使用单元格作为基本/根本组件,以及单向数据流。
JSQDataSourcesKit
在某种意义上来说一直有点实验性和学术性。它不做任何差异化,并且也有独立的表格和集合基础设施,因为它早于那些现代集合视图 API。它主要关注于构建类型安全的数据源,消除与 UITableViewDataSource
和 UICollectionViewDataSource
相关的样板代码。最终,泛型过于笨拙。有关更多详细信息,请参阅我的文章弃用 JSQDataSourcesKit。JSQDataSourcesKit
的正确之处在于使用泛型提供类型安全性的想法,尽管它没有得到很好的执行。
所有这些经验和知识最终促使我编写了这个库 ReactiveCollectionsKit
,它的目标是保留上面库中的所有好想法和设计,同时解决它们的缺点。我编写或维护了所有这些库,所以希望这次能做对!:)
ReactiveLists
的正确之处在于不可变性、声明式 API 和单向数据流。使用 ReactiveLists
,您可以声明式地定义整个集合视图模型,并在底层数据模型发生更改时重新生成它。
同时,IGListKit
非常命令式和可变的。使用 IGListKit
,在您连接 IGListAdapter
和 IGListSectionController
对象后,您可以就地更新节。IGListKit
鼓励不可变的数据模型,但这在 Objective-C 中是不可执行的,API 中也没有强制执行。IGListKit
在某种意义上确实具有单向数据流,但您通过 IGListAdapterDataSource
以命令式的方式提供您的数据,这还需要您手动管理数据模型对象与其对应的 IGListSectionController
对象之间的映射。
ReactiveCollectionsKit
改进了 ReactiveLists
和 IGListKit
采用的方法,并删除或整合了 IGListKit
所需的样板代码。
CellViewModel
是库中的基本或“原子”组件。它封装了单个单元格的所有数据、配置、交互和注册。这类似于 ReactiveLists
。在 IGListKit
中,此组件对应于 IGListSectionController
。IGListKit
的一个缺点是“原子”组件是多个项目的整个节 — 一个节可能只有一个项目,在这种情况下,它更像 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) 他们是否已被删除/移动/添加。 接下来,我们可以确定自从我们上次看到他们以来他们是否发生了变化(使用 ==
)。 这使我们能够确定集合中唯一的某个人何时需要重新加载或刷新。
IGListKit
和 ReactiveLists
都正确地实现了这一点,但它们的实现更加繁琐和手动。 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
协议继承自 DiffableViewModel
和 ViewRegistrationProvider
以完成这些任务。 这允许自动和可定制的差异化以及自动视图注册。 由于 Swift 通过协议扩展的默认实现,你可以免费获得很多默认行为。 如上所述,你可以通过编译器合成的定义免费获得 Equatable
和 Hashable
的一致性。 对于 ViewRegistrationProvider
,你可以免费获得基于类的默认注册。 此功能类似于 ReactiveLists
。 IGListKit
也提供自动注册,但它非常隐式。
本质上,来自集合视图的所有数据源和委托方法都被转发到 CellViewModel
的每个实例。
对于标题、页脚和补充视图,有一个类似的 SupplementaryViewModel
。
IGListKit
尽管有一些 Swift 改进,但仍然受到 Objective-C 类型系统中缺乏表现力的影响。 ReactiveLists
在这方面做得更好,但它的出现早于 Swift 泛型和存在类型的现代改进。 在 ReactiveLists
中,配置单元格需要将 UITableViewCell
或 UICollectionViewCell
强制转换为视图模型的特定单元格类型。 在 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
提供了许多使用泛型的便捷初始化器。 在你没有混合数据类型的情况下,泛型初始化器允许你忽略这个实现细节并为你处理类型擦除。
UICollectionViewCompositionalLayout
, Lickability有兴趣为这个项目做出贡献吗? 请查看下面的指南。
由 Jesse Squires 创建和维护。
在 MIT 许可证下发布。 有关详细信息,请参阅 LICENSE
。
版权所有 © 2019-present Jesse Squires。