IOS知识积累iOS

IGListKit接入小结(一)

2020-02-16  本文已影响0人  奥利奥_2aff

iOS原生端开发过程中, 列表是最常见的需求之一. 随着业务和UI交互设计的迭代, 我们逐渐会接触到这样的需求:

接着我们就遇到这样的问题:

Instagram 团队的开源框架IGListKit是一个非常好用的解决方案.

介绍

IGListKit 可以做什么?

简单地说IGListKit封装了很多友好的API去帮我们适配和更新UICollectionView/UITableView(在4.0版中加入了对UITableView的支持, 但是主要API还是服务于UICollectionView), 它专注于处理列表的数据源和操作行为.

那么IGListKit是如何做到的呢?

如果我们最基本地使用IGListKit, 我们会接触到下面这几个类型:


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;

源码
在这里, 我们看到了另外两个关键类型ListSectionControllerIGListDiffable.
从函数名字和注释我们可以看出,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;

源码
在这里我们看到了一些很熟悉的函数名和属性, 跳过一下像supplementaryViewSourcedisplayDelegate这样还不明确的属性. 我们已经可以猜出ListSectionController做的事情:


ListDiffable

回顾ListAdapterListSectionController的API, 我们已经明白, 我们每次更新列表, 就是我们更新ListDiffable数组. 到现在我们已经知道了, ListDiffable是IGListKit封装的API中列表的数据单位.

那么问题就是, 我们要怎么去生成这个数据单位呢?

查看代码, 其实ListDiffable是一个非常简单的协议:

NS_SWIFT_NAME(ListDiffable)
@protocol IGListDiffable
- (nonnull id<NSObject>)diffIdentifier;
- (BOOL)isEqualToDiffableObject:(nullable id<IGListDiffable>)object;
@end

源码
只有两个API:


怎样接入IGListKit

有了大致了解之后, 我们看一下要怎样接入IGListKit. 这里先以UICollectionView为例.

参考IGListKitdemo, 其中有一个比较简单的例子StoryboardViewController.

在这里我们看到了:


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)
    }

}

源码

可以看出:


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个属性:


到这里, 我们可以知道:

借用一张来自raywenderlich的图:

ListDiffableData, Adapter, SectionController, Cell之间的关系

所以我们需要做的事情, 小结就是:


IGListKit 4.0 新增对于UITableView的支持

上面我们讨论了CollectionView场景接入IGListKit, 而在4.0更新之后, IGListKit甚至可以支持TableView的组件更新.

而这是通过子模块IGListDiffKit实现的.

我们会在ListDiffableKit中接触以下类型:

这两个类型存储了列表组件变化的数据, 而它们的关系就类似IndexPathIndexSet的关系. 我们先只看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:


我们可以在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已经不是对应Section的数据单位!
因为UITableView并没有对应的ListSectionController去专门处理ListDiffable数据.


引发的思考

接入IGListKit后, 代码结构发生了以下改善:

那么, 难道接入IGListKit就只有好处吗?

看看接入IGListKit的副作用:

接入IGListKit也是有一定成本的.

既然如此, 接入IGListKit的取舍是什么?

以上是对IGListKit接入的第一步小结, 随着对列表开发的深入, 我们还需要知道IGListKit的其他API及其运作机制. 如:

后面我们将会继续探讨IGListKit.

上一篇 下一篇

猜你喜欢

热点阅读