UICollectionView/UITableView
作为一个 iOS 开发者,在日常开发中少不了与 UITableView/UICollectionView 打交道。因为复用池的存在,即使在处理大量数据的情况下,它们仍能保持较低的内存占用,而简单的 DataSource/Delegate 设计方式,可以让我们只需要几行代码就可以完成对 UITableView/UICollectionView 数据和交互的相关配置。由于 UITableView 对界面布局的限制,你无法在 UITableView 中自定义布局,比如瀑布流之类的样式。于是苹果在 iOS6.0 中推出了 UICollectionView , Steipete 为了支持更老旧的系统,自己写了个 PSTCollectionView ,通过这个框架可以大致了解 UICollectionView 的内部实现。 虽然 UICollectionView 通过把 Layout 抽出来的方式以提供自定义布局,但是在处理数据时本质上和 UITableView 还是相同的。 日常使用 UICollectionView/UITableView 时都会遇到下面几个问题:
- 复用流程。如果想在
UITableView/UICollectionView中复用Cell,那么你需要先调用register(_:forCellReuseIdentifier:)注册对应的Cell类型,然后在对应的方法中调用dequeueReusableCell(withIdentifier:)从复用池中取出或者生成一个新的Cell来使用。如果在获取Cell前忘记调用register进行注册,那么你就会收获一个应用崩溃,至于为什么需要先进行注册, register(_:forCellReuseIdentifier:) 有简单说明; - 不支持
diff。在数据源变化时我们需要更新对应的界面,而苹果本身并没提供一套相关的 API 给我们计算相关的删除,插入或者更新的位置(indexPath),我们需要自己去计算,找出更新的数据源,然后删除,插入或者更新对应的Cell。在大多数情况下,由于有复用机制的存在,我们直接使用reloadData也不会有什么问题,复用机制使得在reloadData时只会对显示在屏幕上的Cell进行处理,需要处理视图数量并不多,但是应该有更好的办法来支持diff,不是么?( PS :苹果在 WWDC19 上推出 Diffable Data Source 功能,支持局部刷新); - 模块的隔离。在界面变得复杂时,我们需要处理数据的获取,处理
Cell的配置,处理埋点统计之类的功能,对于这些模块的隔离,系统本身的框架并没有提供这么一套东西给我们,所以很多时候会写在ViewController里,于是变成了Massive View Controller。如果自行设计一套方案,很容易弄出一套不但使用起来非常难受,而且还难以迁移的框架。
IGListKit
IGListKit 是 Instagram 推出的一套数据驱动的 UICollectionView 框架,以此来创建快速灵活的列表界面,为什么选择 UICollectionView 而不是 UITableView ,因为 UICollectionView 支持自定义布局,比 UITableView 更加灵活。 IGListKit 的主要特性如下:
- 不再需要调用
performBatchUpdates(_:, completion:)或者reloadData(); - 更好的架构以复用
Cell和组件; - 支持多种数据类型;
- 与
diff算法解耦; - 全面的单元测试;
- 支持对自己的数据模型进行自定义
diff操作; - 使用
UICollectionView更加简单; - 可扩展的 API ;
- 使用
Objective-C编写,同时完全支持Swift。
IGListKit 的数据处理流程如下:

数据经由 Adapter 进行处理后转给对应的 SectionController ,而 SectionController 则会根据不同的数据类型来返回不同的 Cell 。整个过程中, UIViewController 只和 Adapter 和 Updater 进行交互,并根据不同的数据类型返回不同的 SectionController 。而对于 Cell 的处理则完全交给 SectionController 来进行,这一步的好处是使得 SectionController 可以进行复用,扩大复用粒度,同时 SectionController 的复用粒度也是比较合适的。因为我们可能需要对不同的 UIViewController 进行配置,但是有很大可能它们的 Cell 显示方式都是相同,只是其它一些逻辑或者 UI 不同。我们也可以经由 SectionController 进行组装,合成不同的界面。
使用 IGListKit 时,创建一个列表界面就会变得非常容易。 首先我们需要创建一个 IGListSectionController 的子类,需要重写的方法有两个:
cellForItemAtIndex:;sizeForItemAtIndex:。 下面以LabelSectionController为例子进行说明:
1
2
3
4
5
6
7
8
9
class LabelSectionController: ListSectionController {
override func sizeForItem(at index: Int) -> CGSize {
return CGSize(width: collectionContext!.containerSize.width, height: 55)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
return collectionContext!.dequeueReusableCell(of: MyCell.self, for: self, at: index)
}
}
可以看到 Cell 的配置和数据的处理都交给了 SectionController 来进行。完成 LabelSectionController 的配置后,需要在 UIViewController 或者其它什么地方把各个模块串联起来:
1
2
3
4
5
6
let layout = UICollectionViewFlowLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
let updater = ListAdapterUpdater()
let adapter = ListAdapter(updater: updater, viewController: self)
adapter.collectionView = collectionView
可以看到 UICollectionView 设置布局 Layout 部分没有变动,但是使用了 IGListAdapter 和 IGListAdapterUpdater 将 UIViewController 和 UICollectionView 串起来。这里使用了默认的 UICollectionViewFlowLayout 和 IGListAdapterUpdater ,你也可以配置自定义的类来使用一些高级特性。
跟 UICollectionView 类似,需要给 adapter 设置 dataSource :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
adapter.dataSource = self
// IGListAdapterDataSource 相关方法
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
// this can be anything!
return [ "Foo", "Bar", 42, "Biz" ]
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
if object is String {
return LabelSectionController()
} else {
return NumberSectionController()
}
}
func emptyView(for listAdapter: ListAdapter) -> UIView? {
return nil
}
你可以返回任何数据类型,只要它们支持 IGListDiffable 协议就可以了。
不可变
返回的数据应该是不可变的。如果你返回可变的对象且你会对它们进行编辑, IGListKit 就无法正确地计算出它们之间的差异。这是因为对象已经发生了改变。因此,该对象的改动就会丢失掉。你可以返回一个新的,不可变的对象,而且支持了 IGListDiffable 协议。
IGListKit 的基本用法和大概架构已经介绍完毕,更详细的介绍可以看官方文档: IGListKit Reference ,后续会对源码进行解析。
Comments powered by Disqus.