實質(zhì)上是 UICollectionView 與 NSFetchedResultsController 配合期虾,在去年遇到這個問題時镶苞,當(dāng)時進行了一番嘗試無解,找到了倆年前 @ash furrow 的方案宾尚,但原來的方案中無法同時處理 section 和 cell 的變化,我針對這個缺陷做了改進牛郑,但復(fù)雜而且難以使用敬鬓;后來 @SixtyFrames?修正了這個缺陷,并且優(yōu)化了原方案的邏輯础芍,幾乎不需要改動就可使用杈抢。原方案的作者后來停止了維護該方案,并推薦了更優(yōu)雅的解決方案:JSQDataSourcesKit仑性,使用前提是 iOS 8和 Swift 2.0惶楼。?
我上周 fork 了這個庫,原本還打算給它貢獻點這個話題方面的代碼诊杆,結(jié)果發(fā)現(xiàn)它已經(jīng)處理好了歼捐,而且代碼封裝得很好,我目前可寫不出這種水平的代碼晨汹。使用 Swift 有三個月左右了豹储,越來越喜歡這個語言了,最近連 OC 都不會寫了淘这。剛開始總會接觸到 protocol 的話題剥扣,但不是很理解慨灭,最近寫了幾個動畫,對 protocol 有點感受了,不過這個庫我目前看得有點吃力,不知道為何要那樣封裝宋彼,等三個月再看我的水平是否足夠±晨玻現(xiàn)成的解決方案是有了碴卧,但思路還是值得記錄的烫葬。
為 NSFetchedResultsController 對象提供 delegate 后,就能夠跟蹤數(shù)據(jù)的變化并更新視圖了, delegate 對象需要實現(xiàn)下面的方法:
-controllerWillChangeContent:
-controller:didChangeSection:atIndex:forChangeType:
-controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:
-controllerDidChangeContent:
當(dāng) NSManagedObjectContext 中對象發(fā)生了變化后,上面四個方法除了中間的兩個按照變化的情況來調(diào)用之外,基本上是按照順序來調(diào)用的。
對于 UICollectionView,來看看解決思路,ash furrow 指出問題的關(guān)鍵在于:
The trick is to queue the updates made through the NSFetchedResultsControllerDelegate until the controller finishes its updates. UICollectionView doesn't have the same beginUpdates and endUpdates that UITableView has to let it work easily with NSFetchedResultsController, so you have to queue them or you get internal consistency runtime exceptions.
那么解決的方法就是在上面中間兩個 didChangeXXX 方法中搜集變化的內(nèi)容汰寓,然后在最后的 didChangeContent 里使用 UICollectionView 的 performBatchUpdates: 方法來集中更新 UI俺孙。?
原方案沒有考慮到一些情況而無法處理同時發(fā)生了 section 和 cell 變化的情況荣茫,比如我的需求是批量操作多個 section 內(nèi)的 cell旨剥,例如优幸,選中不同 section 內(nèi)的 cell备典,這包括了某個 section 內(nèi)的全部 cell 以及其他 section 中部分 cell,然后新建一個 section,如下圖:
那么 delegate 將會接受到以下通知满哪,log 里我打印出了動作類型以及位置變化哈恰,使用 S 代表了 cell 所在的 section锌云,I 代表 row兼贡。
Insert Section at Index: 4
Delete Section at Index: 2
Move Cell from S1I8 -> S4I5
Move Cell from S2I1 -> S4I1
Move Cell from S2I0 -> S4I0
Move Cell from S4I1 -> S4I2
Move Cell from S2I2 -> S4I4
Move Cell from S3I1 -> S4I3
上面的這些變化需要我們在 controllerDidChangeContent: 里先對這些變化進行分類和過濾后再來更新 UI涉兽。你需要嘗試一些操作來測試 UICollectionView 是怎么判定操作的類型的,依據(jù)這個才好對操作進行分類和過濾篙程。
操作類型有這么四種:
NSFetchedResultsChangeInsert?
NSFetchedResultsChangeDelete
NSFetchedResultsChangeMove
NSFetchedResultsChangeUpdate
NSFetchedResultsChangeMove 這種操作類型的通知里會包含目標的源位置 fromIndexPath 和新位置 toIndexPath虱饿,根據(jù)對這種操作類型的處理方式不同,有兩種方法來處理和過濾這些變化:
1. @SixtyFrames?的方案:
過濾階段:
首先對標記為 NSFetchedResultsChangeMove 的操作進行過濾:
- fromIndexPath 的 section 在被刪除的 section 集合里并且 toIndexPath 的 section 不在新增的 section 集合里,則被視為 NSFetchedResultsChangeInsert礁苗,并將 toIndexPath 納入該操作類型的集合里。翻譯一下就是:從被刪除的 section 移動到另外一個已知的 section 被判定為 insert。
上面例子中的沒有符合這個條件的昵时。
- fromIndexPath 的 section 不在被刪除的 section 集合里并且 toIndexPath 的 section 在新增的 section 集合里,被視為 NSFetchedResultsChangeDelete壹甥,并將 fromIndexPath 納入該操作類型的集合中救巷。意思就是:整個 section 沒有被刪的而且移動到新 section 的被判定為 delete。
上面的例子中有三條符合: S1I8 -> S4I5, S3I1 -> S4I3, S4I1 -> S4I2句柠。S1I8浦译,S3I1,S4I1 這些 NSIndexPath 被標記為要被刪除的目標溯职。
- fromIndexPath 的 section 不在被刪除的 section 集合里并且 toIndexPath 的 section 不在新增的 section 集合里精盅,才被視為 NSFetchedResultsChangeMove。意思就是:從一個已知的 section 移動到另外一個已知的 section 才被判定為真正的 move谜酒。
上面的例子沒有符合條件的叹俏。
其次,對標記為 NSFetchedResultsChangeDelete 的操作進行過濾甚带,去除那些目標 section 在被刪除的 section 集合里的 NSIndexPath她肯。這么做的原因是,某個 section 要被刪除的話鹰贵,該 section 內(nèi)的所有 cell 都會被移除晴氨,但不用分別刪除 section 和刪除 cell 這樣的重復(fù)操作,因此必須把該 section 內(nèi)的所有 NSIndexPath 過濾掉碉输。
上面的例子中沒有 NSIndexPath 被移除籽前。
最后,對標記為 NSFetchedResultsChangeInsert 的操作進行過濾,去除那些目標 section 是新增 section 的 NSIndexPath枝哄。
上面的例子里沒有屬于 NSFetchedResultsChangeInsert 的 NSIndexPath肄梨,因此什么也不會移除。
接下來要對上面的過濾結(jié)果進行針對性的操作挠锥,更新 UI 要按照一定的順序來众羡,這點在 performBatchUpdates:completion: 的文檔里有說明:delete 操作必須在 insert 之前,因為 insert 時的位置是相對于 delete 后再次計算的蓖租,這與我們之前看到的 log 恰好是反過來的粱侣。
另外,section 的變化會同時處理該 section 內(nèi)所有 cell 的變化蓖宦,所有只需要對 section 進行更新即可齐婴。
- 先處理 section 的變化:先刪除被標記的 section(可能有多個),然后添加被標記的 section(一般情況下只有一個)稠茂。
上面的例子 Section 2 被整體刪除柠偶,然后添加 Section 4。
- 然后處理不在 section 變化范圍內(nèi)的漏網(wǎng)之魚睬关,處理順序是诱担,先處理刪除,然后是添加共螺,移動和更新操作的順序無所謂该肴,因為這兩個不影響整體的布局。
上面的例子里被判定為 delete 的三個目標將被刪除藐不。
至此結(jié)束匀哄。
2. JSQDataSourcesKit
JSQDataSourcesKit 的方式與上面截然不同,簡直有種做奧賽題使用常規(guī)方法和特殊方法的區(qū)別:把 NSFetchedResultsChangeMove 拆分為先 Delete 再 Insert雏蛮,這樣一來所有的操作只剩下 Delete 和 Insert(Update 不影響大局)涎嚼,因此只需要對搜集的變化進行分類而不需要過濾,而且在處理上不需要依賴特定的順序挑秉。在處理時法梯,需要先處理 object 的變化,再處理 section 的變化犀概。處理 object 的變化時立哑,遇到 Move 操作時,先刪除原來的目標姻灶,在添加新的目標铛绰;而處理 section 的變化時,不處理 Move 操作产喉。
簡潔有力捂掰!但老實說敢会,除了拆分這個操作明白,后來對變化的處理不明白这嚣。