iOS原生端开发过程中, 列表是最常见的需求之一. 随着业务和UI交互设计的迭代, 我们逐渐会接触到这样的需求:
- 列表中出现多种不同样式的Cell
- 列表中出现复杂的Cell插入, 更新, 删除, 移位动画
接着我们就遇到这样的问题:
- 同一列表中适配多种Cell, 导致dataSource部分代码臃肿不好维护
- 同一列表中复杂的Cell带来同样多的回调适配, 进一步增加臃肿度和维护难度
- 复杂的列表更新策略配合多种不同的数据类型, 导致批量更新列表同样麻烦
- 针对某些Cell组合的业务逻辑复用
Instagram 团队的开源框架IGListKit是一个非常好用的解决方案.
介绍
IGListKit 可以做什么?
简单地说IGListKit封装了很多友好的API去帮我们适配和更新UICollectionView
/UITableView
(在4.0版中加入了对UITableView
的支持, 但是主要API还是服务于UICollectionView
), 它专注于处理列表的数据源和操作行为.
那么IGListKit是如何做到的呢?
如果我们最基本地使用IGListKit, 我们会接触到下面这几个类型:
- ListAdapter
- ListSectionController
- ListDiffable
ListAdapter
ListAdapter
是我们调用更新UI的API的入口, 它帮我们桥接了UICollectionView
的一些API. 在这个类型中有以下几个关键API:
@property (nonatomic, nullable, weak) UIViewController *viewController;
@property (nonatomic, nullable, weak) UICollectionView *collectionView;
@property (nonatomic, nullable, weak) id <IGListAdapterDataSource> dataSource;
@property (nonatomic, nullable, weak) id <IGListAdapterDelegate> delegate;
@property (nonatomic, nullable, weak) id <UICollectionViewDelegate> collectionViewDelegate;
- (void)performUpdatesAnimated:(BOOL)animated completion:(nullable IGListUpdaterCompletion)completion;
- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion;
- (void)reloadObjects:(NSArray *)objects;
源码
从名字上我们就可以看出, ListAdapter
其实做了一些本来是UICollectionView
做的事情, 比如更新行为.
而IGListKit的example中也告诉了我们这句话:使用ListAdapter
去更新界面而不要再自己调用UICollectionView
的接口.
除此以外, 我们还看到了dataSource
, delegate
, scrollDelegate
这类原来在UICollectionView
上的属性, 实际上它就是桥接了对应的属性.
我们还可以见到一个viewController
的属性, 后面我们再讨论为什么会出现这个属性.
IGListAdapterDataSource
我们可以看到, 这是一个协议. 它非常简单, 只有几个的API:
- (NSArray<id <IGListDiffable>> *)objectsForListAdapter:(IGListAdapter *)listAdapter;
- (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object;
- (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter;
源码
在这里, 我们看到了另外两个关键类型ListSectionController
和IGListDiffable
.
从函数名字和注释我们可以看出,dataSource
是我们提供另外两个关键类型的数据的地方, 以及提供列表没有数据时候的提示UI组件的地方.(上面代码块中注释被删掉了)
ListSectionController
ListAdapter
是我们发起更新的地方, 那么ListSectionController
就是我们做行为适配的地方了.
上面我们已经可以看到, 在IGListAdapterDataSource
协议中我们需要返回一个ListSectionController
的实例. 而对这个函数里面除了提供了一个ListAdapter
的实例变量, 和一个id
类型的变量.
我们不难理解这个listAdapter
, 那么这个object
变量又是做什么的呢? 它和ListSectionController
又有什么联系呢?
先给出直接答案:
这个object
就是我们另一个关键类型ListDiffable
. 而我们在这个函数中到底返回怎么样的ListSectionController
就取决于我们要对什么样的ListDiffable
数据进行适配.
接着看一下ListSectionController
的部分API:
- (NSInteger)numberOfItems;
- (CGSize)sizeForItemAtIndex:(NSInteger)index;
- (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index;
- (void)didUpdateToObject:(id)object;
- (void)didSelectItemAtIndex:(NSInteger)index;
- (void)didDeselectItemAtIndex:(NSInteger)index;
- (void)didHighlightItemAtIndex:(NSInteger)index;
@property (nonatomic, weak, nullable, readonly) UIViewController *viewController;
@property (nonatomic, weak, nullable, readonly) id <IGListCollectionContext> collectionContext;
@property (nonatomic, assign) UIEdgeInsets inset;
@property (nonatomic, assign) CGFloat minimumLineSpacing;
@property (nonatomic, assign) CGFloat minimumInteritemSpacing;
@property (nonatomic, weak, nullable) id <IGListSupplementaryViewSource> supplementaryViewSource;
@property (nonatomic, weak, nullable) id <IGListDisplayDelegate> displayDelegate;
源码
在这里我们看到了一些很熟悉的函数名和属性, 跳过一下像supplementaryViewSource
和displayDelegate
这样还不明确的属性. 我们已经可以猜出ListSectionController
做的事情:
- 适配
UICollectionViewCell
的数量 - 适配对应的
UICollectionViewCell
实例 - 适配Cell的大小
- 适配Cell以及本Section的间距
- 适配用户操作行为以及事件响应行为
- 可以获取当前所在的
UIViewController
ListDiffable
回顾ListAdapter
和ListSectionController
的API, 我们已经明白, 我们每次更新列表, 就是我们更新ListDiffable
数组. 到现在我们已经知道了, ListDiffable
是IGListKit封装的API中列表的数据单位.
那么问题就是, 我们要怎么去生成这个数据单位呢?
查看代码, 其实ListDiffable
是一个非常简单的协议:
NS_SWIFT_NAME(ListDiffable)
@protocol IGListDiffable
- (nonnull id<NSObject>)diffIdentifier;
- (BOOL)isEqualToDiffableObject:(nullable id<IGListDiffable>)object;
@end
源码
只有两个API:
-
diffIdentifier
明显是用于标识这条数据唯一性 - 函数
isEqualToDiffableObject(:)
则是具体实现如何判别这条数据和另一条数据不一样.
怎样接入IGListKit
有了大致了解之后, 我们看一下要怎样接入IGListKit. 这里先以UICollectionView为例.
参考IGListKit的demo, 其中有一个比较简单的例子StoryboardViewController.
在这里我们看到了:
-
ListAdapter
的创建以及调用 - 在协议函数里返回了一个
ListSectionController
的子类StoryboardLabelSectionController
- 实现了
ListDiffable
协议的数据Person
ListAdapter的使用:
/*
创建的时候就需要传入viewController, 以及一个updater, 这个updater暂时不讨论.
*/
lazy var adapter: ListAdapter = {
return ListAdapter(updater: ListAdapterUpdater(), viewController: self)
}()
/*
必要参数赋值, dataSource, 托管的collectionView
*/
adapter.collectionView = collectionView
adapter.dataSource = self
/*
在回调中更新UICollectionView.
可以通过adapter找到对应的section, 修改数据后调用adapter的performUpdates函数.
*/
func removeSectionControllerWantsRemoved(_ sectionController: StoryboardLabelSectionController) {
let section = adapter.section(for: sectionController)
people.remove(at: Int(section))
adapter.performUpdates(animated: true)
}
ListSectionController的使用:
接着我们看一下这个StoryboardLabelSectionController
的代码
final class StoryboardLabelSectionController: ListSectionController {
private var object: Person?
weak var delegate: StoryboardLabelSectionControllerDelegate?
override func sizeForItem(at index: Int) -> CGSize {
return CGSize(width: (self.object?.name.count)! * 7, height: (self.object?.name.count)! * 7)
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
guard let cell = collectionContext?.dequeueReusableCellFromStoryboard(withIdentifier: "cell",
for: self,
at: index) as? StoryboardCell else {
fatalError()
}
cell.text = object?.name
return cell
}
override func didUpdate(to object: Any) {
self.object = object as? Person
}
override func didSelectItem(at index: Int) {
delegate?.removeSectionControllerWantsRemoved(self)
}
}
可以看出:
-
StoryboardLabelSectionController
持有了Person
对象, 就是在didUpdate(to:)函数中获得的. 而在适配Cell的时候用到了它. - 在这个例子中, 每个Section中只有1条数据. 但是其实SectionController控制的是
UICollectionView
中的Section, 所以也可以在这里适配多个数据或者多种Cell. - Cell的点击回调发生在
didSelectItem(at:)
中, 此处用了delegate作为回调方式. 而我们上面已经知道在ListSectionController
中有一个属性viewController
, 也可以通过这个属性实现回调.
Person:
final class Person: ListDiffable {
let pk: Int
let name: String
init(pk: Int, name: String) {
self.pk = pk
self.name = name
}
func diffIdentifier() -> NSObjectProtocol {
return pk as NSNumber
}
func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
guard let object = object as? Person else { return false }
return self.name == object.name
}
}
可以看到Person
类中除了ListDiffable
协议的2个必需的函数以外, 还有2个属性:
-
pk
属性被用作唯一标识 -
name
属性被用于在适配Cell的时候加载显示 -
isEqual(toDiffableObject:)
中做了类型对比和name
属性的对比
到这里, 我们可以知道:
-
ListAdapter
的数据源就是实现了ListDiffable
协议的数据的数组, 我们更新CollectionView需要调用ListAdapter
的函数 -
ListDiffable
类型对应的是CollectionView中的Section单元的数据, 它里面的数据也对应这个Section里面的Cell -
ListSectionController
把相应ListDiffable
数据适配成对应的Section, 在它这里适配Cell的样式和回调
借用一张来自raywenderlich的图:
所以我们需要做的事情, 小结就是:
- 用
ListAdapter
桥接ViewController和CollectionView - 把原来CollectionView的
dataSource
的协议函数改成ListAdapter
的dataSource
协议函数 - 给原来的数据源类型实现
ListDiffable
协议, 记得ListDiffable
数据对应的是Section - 把Cell的适配和回调代码迁移到
ListSectionController
的子类中
IGListKit 4.0 新增对于UITableView的支持
上面我们讨论了CollectionView场景接入IGListKit, 而在4.0更新之后, IGListKit甚至可以支持TableView的组件更新.
而这是通过子模块IGListDiffKit实现的.
我们会在ListDiffableKit中接触以下类型:
- ListIndexPathResult
- ListIndexSetResult
这两个类型存储了列表组件变化的数据, 而它们的关系就类似IndexPath
和IndexSet
的关系. 我们先只看ListIndexPathResult
@property (nonatomic, copy, readonly) NSArray<NSIndexPath *> *inserts;
@property (nonatomic, copy, readonly) NSArray<NSIndexPath *> *deletes;
@property (nonatomic, copy, readonly) NSArray<NSIndexPath *> *updates;
@property (nonatomic, copy, readonly) NSArray<IGListMoveIndexPath *> *moves;
@property (nonatomic, assign, readonly) BOOL hasChanges;
- (nullable NSIndexPath *)oldIndexPathForIdentifier:(id<NSObject>)identifier;
- (nullable NSIndexPath *)newIndexPathForIdentifier:(id<NSObject>)identifier;
- (IGListIndexPathResult *)resultForBatchUpdates;
源码
可以看到它这几个关键API:
- 属性
inserts
,deletes
,updates
,moves
, 分别对应插入, 删除, 更新, 移动的数据 - 属性
hasChanges
代表这条结果和列表上一次的结果是否出现不同 - 函数
oldIndexPathForIdentifier(:)
和newIndexPathForIdentifier(:)
可以根据唯一标识找到更新前/后, 其在列表中对应的IndexPath - 函数
resultForBatchUpdates
, 返回可以用于安全更新TableView或CollectionView的ListIndexPathResult
实例
我们可以在demo中找到一个对应的例子DiffTableViewController, 它就借助了ListIndexPathResult
去更新UITableView:
@objc func onDiff() {
let from = people
let to = usingOldPeople ? newPeople : oldPeople
usingOldPeople = !usingOldPeople
people = to
// 调用全局函数, 传入更新前后的数据源, 获得ListIndexPathResult实例
let result = ListDiffPaths(fromSection: 0, toSection: 0, oldArray: from, newArray: to, option: .equality).forBatchUpdates()
// 调起tableView的批量更新
tableView.beginUpdates()
// 调起tableView的deleteRows, 从result的deletes属性获得被删除的IndexPath数组
tableView.deleteRows(at: result.deletes, with: .fade)
// 调起tableView的insertRows, 从result的inserts属性获得被删除的IndexPath数组
tableView.insertRows(at: result.inserts, with: .fade)
// 由于UITableView没有批量移动IndexPath的API, 所以要遍历result的moves属性, 逐个执行tableView的moveRow(at:, to:)函数
result.moves.forEach { tableView.moveRow(at: $0.from, to: $0.to) }
// 结束批量更新
tableView.endUpdates()
}
我们可以到, 仅仅使用ListIndexPathResult
, 我们不需要借助ListAdapter
也可以顺利更新列表.
我们需要做的关键点是:
- 使用
ListDiffable
数据作为数据源 - 获得更新前和更新后的
dataSource
数组和对应的section - 调用
ListDiffPaths()
函数得到ListIndexPathResult
- 调起TableView/CollectionView的批量更新函数, 取出变更的
IndexPath
数据进行对应操作
注意:
在这个例子中ListDiffable
已经不是对应Section的数据单位!
因为UITableView并没有对应的ListSectionController
去专门处理ListDiffable
数据.
引发的思考
接入IGListKit后, 代码结构发生了以下改善:
- 通过
ListSectionController
对不同类型的Cell进行单独适配, 减轻了dataSource和delegate的负担 - 通过
ListAdapter
更新CollectionView让我们不需要再自行维护具体的数据变化 - 通过
ListIndexPathResult
/ListIndexSetResult
也可以快速地让TableView的更新变得简单化 - 如果遇到需要复用的Cell组合业务逻辑, 可以直接复用
ListSectionController
- 接入IGListKit无需改变Cell的代码, 也不影响CollectionView和UITableView本身在其
superview
上的布局状态
那么, 难道接入IGListKit就只有好处吗?
看看接入IGListKit的副作用:
- 使用
ListSectionController
适配对应的ListDiffable
数据, 项目整体代码量增加, 会延长开发周期. - CollectionView界面迭代后需要进行大量代码迁移, 如果界面中业务逻辑比较复杂容易引发错误, 需要重新测试.
- 如果原界面是通过UITableView实现的话, 想要得到
ListSectionController
带来的便利, 需要把所有涉及的TableViewCell改成CollectionViewCell. - 必须把数据源换成
ListDiffable
类型. 因此要对原数据类型进行改造. 如果不想/无法改造原类型代码, 则需要另外定义新的类型.
接入IGListKit也是有一定成本的.
既然如此, 接入IGListKit的取舍是什么?
- 如果只是有复杂的列表更新需求, 但是没有复杂的Cell适配, 优先使用ListDiffableKit.
- 遇上复杂Cell适配情况或者需要复用固定的Cell组合业务, 使用
ListSectionController
. 如果是界面重构, 预留时间做测试. - 如果使用Swift开发, 优先使用extension给原来的Model添加
ListDiffable
协议, 这样可以避免修改原Model的代码. - 如果使用了OC开发, 原来的Model不方便改造, 考虑定义新的类型作为数据源, 但是需要更新对应Cell的代码.
以上是对IGListKit接入的第一步小结, 随着对列表开发的深入, 我们还需要知道IGListKit的其他API及其运作机制. 如:
-
ListDiffable
作为IGListKit的基础数据源, 到底有什么意义. - IGListKit通过什么方式帮我们处理了那些复杂的数据更新逻辑.
-
IGListKit是一个高性能体的框架, 到底现在什么地方.
...
后面我们将会继续探讨IGListKit.