簡單介紹
IGListKit是Instagram推出的新的UICollectionView
框架,使用資料驅動,旨在創造一個更快更靈活的列表控制元件。
github地址:https://github.com/Instagram/IGListKit
這個全新的控制元件一出來,我就趕快投入實踐了一把。
先談一談我對這個控制元件的結論:這個框架設計的非常好,完美符合高內聚、低耦合。IGListKit 是一個很典型的使用 Objective-C 開發的,但卻是個偏向使用 Swift 語言開發者的一個 UI 元件庫。
使用過程也面臨了一些疑惑,先談一下使用收穫:
- 它的優勢在於flexible,比起原來的
UICollectionView
,在使用上更加靈活,在資料驅動上做的更好。 - 這個框架在fast上體現的還不夠,但不妨礙我們自己進行下一步優化。
先看看IGListKit的結構
在原來的UICollectionViewController裡的寫法,我們一定都會實現UICollectionDataSource和UICollectionViewDelegate。
不過在IGListKit的實戰過程中,你會發現似乎不用在ViewController中實現相關協議,取而代之的是SectionController來實現對應的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class DemoSectionController: IGListSectionController, IGListSectionType{ var object: DemoItem? func numberOfItems() -> Int { return 1 } func sizeForItem(at index: Int) -> CGSize { return CGSize(width: collectionContext!.containerSize.width, height: 55) } func cellForItem(at index: Int) -> UICollectionViewCell { let cell = collectionContext!.dequeueReusableCell(of: LabelCell.self, for: self, at: index) as! LabelCell cell.label.text = object?.name return cell } } |
這裡直接取了官方的Demo裡的其中一個SectionController作為例子。其實UICollectionDataSource
和UICollectionViewDelegate
都交給了Adapter
這個介面卡中。我們來看一下IGAdapter.m
檔案中的原始碼:
當我們為介面卡繫結collectionView時,呼叫如下方法
1 2 3 4 5 6 7 8 9 |
- (void)setCollectionView:(IGListCollectionView *)collectionView { if (_collectionView != collectionView || _collectionView.dataSource != self) { _collectionView = collectionView; _collectionView.dataSource = self; [self updateCollectionViewDelegate]; [self updateAfterPublicSettingsChange]; } } |
其中self是指介面卡物件。
接著介面卡作為實現資料來源協議的物件,我們來看一下它是怎麼聯絡SectionController群的。
1 2 3 4 5 6 7 8 9 |
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { IGListSectionController *sectionController = [self.sectionMap sectionControllerForSection:indexPath.section]; _isDequeuingCell = YES; UICollectionViewCell *cell = [sectionController cellForItemAtIndex:indexPath.item]; _isDequeuingCell = NO; [self mapCell:cell toSectionController:sectionController]; return cell; } |
可以看到adapter通過遍歷自己的sectionController的map來達到UICollectionView的資料來源在cellForItem如何選擇對應的sectionController。
坦白說,這樣做,給人一種全新的思路,而且以後就算自己實現其實也並不複雜,可以參考其設計。
WorkRange能做的事
什麼是WorkRange?還是用Github的官方介紹說的更快,更清楚。
大體就是說,我們可以指定左右的Working區間,幹一些準備工作。
官網寫的不多,只說了我們可以幹事,具體幹啥事,在我的個人實踐中,我對它使用的理解是這樣的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
func updateItem(withItems items:Array) { /* 假設我們在viewController中更新資料來源,item為資料模型 Items = [CommentItem(name: "Mike", comment: ""), CommentItem(name: "Chen", comment: ""), ....] */ commentGroup = CommentGroup(Items: Items) let queue = DispatchQueue(label: "myBackgroundQueue") queue.async { for item in Items { let layout = CommentMainItemLayout(commentItem: item) item.layout = layout } self.commentModels.append(self.commentGroup!) DispatchQueue.main.async { [weak self] in self?.commentAdapter.performUpdates(animated: true, completion: nil) } } } |
而將預下載或者預渲染工作放在workRange中。
1 2 3 4 5 6 7 8 9 |
func listAdapter(_ listAdapter: IGListAdapter, sectionControllerWillEnterWorkingRange sectionController: IGListSectionController) { for url: object.urls { ImageCache.setImage(withUrl:url) //如果需要預渲染,可自行設定 } } func listAdapter(_ listAdapter: IGListAdapter, sectionControllerDidExitWorkingRange sectionController: IGListSectionController) { ImageCache.cancel() } |
Display Delegate
我還沒來得及用到Display Delegate,但我覺得它非常適合在顯示文字的控制元件上使用非同步繪製
我們先來看一看它的呼叫順序
- func cellForItem(at index: Int) -> UICollectionViewCell
- func listAdapterwillDisplay
- func listAdapterdidEndDisplaying
可以發現cellForItem在willDisplay前面,於是我會選擇在cellForItem執行非同步繪製。
在listAdapterdidEndDisplaying暫停非同步繪製,最大程度上防止滑動速度過快,導致白白浪費去執行繪製任務。
和想象不一樣的資料驅動
當初看到github中官方給的圖是這樣的:
我以為IGListKit裡的資料驅動是類似雙向繫結的結構,更新時不用手動顯式的呼叫Update,可實際修改資料來源模型,還是要顯式呼叫
adapter.performUpdates(animated: true, completion: nil)
而這句程式碼對應的就是
1 2 3 4 5 6 |
/** Perform an update from the previous state of the data source. This is analagous to calling -[UICollectionView performBatchUpdates:completion:]. open func performUpdates(animated: Bool, completion: IGListKit.IGListUpdaterCompletion? = nil) |
為什麼稱為Never Call呢?
再來看一下Diff演算法
簡單來說這個演算法就是計算tableView或者collectionView前後資料變化增刪改移關係的一個演算法,時間複雜度是O(n),算是IGListKit的特色特點之一。
其實這個演算法單獨拿出來不只可以計算collectionView模型,稍加改造,也適用於其他模型或者檔案的變化
使用的是Paul Heckel 的A technique for isolating differences between files 的演算法,這份paper是收費。
不過這並不妨礙我們直接看原始碼,我們可以看一下IGListDiff.mm檔案,該演算法使用C++來編寫。
主要是通過hashtable和新舊的兩個陣列結構:
用簡單的例子來說,這裡我模擬的是從假設原來的 1,2,4,1的舊資料模型到新的1,2,3,5的資料模型的變化過程,假想成Swift中程式碼,應該是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let oldModel = [ Num(id: 1, name: "1"), Num(id: 2, name: "2"), Num(id: 3, name: "4"), Num(id: 4, name: "1"), ] let newModel = [ Num(id: 1, name: "1"), Num(id: 2, name: "2"), Num(id: 3, name: "3"), Num(id: 4, name: "5"), ] let result = IGListDiffPaths(0, 0, from, to, .equality).forBatchUpdates() tableView.beginUpdates() tableView.deleteRows(at: result.deletes, with: .fade) tableView.insertRows(at: result.inserts, with: .fade) for move in result.moves { tableView.moveRow(at: move.from, to: move.to) } tableView.endUpdates() |
首先oldIndexs是一個棧的結構,過程是先遍歷新陣列,將陣列裡模型的id對應的hash值作為key,找到對應的Num成員物件(實際程式碼中為entry,可以理解為一種抽象)的oldIndexs棧存入NSNotFound。
再遍歷舊陣列,拿例子來說,就是將陣列裡模型的id 對應的hash值作為key,找到對應的Num成員物件裡的oldIndexs棧增加舊陣列的下標值。
如果是新增加的,那麼在hashtable中key對應的value存入的Num成員物件就是notfound。
這樣演算法如圖使用的資料結構(已簡化,實際稍複雜些),可以繫結新舊陣列的成員的對應關係,包括成員間的移動增加刪除修改關係,對於像TableView或者CollectionView非常適合不過。