UICollectionView 與 Core Data 配合

實質上是 UICollectionView 與 NSFetchedResultsController 配合,在去年遇到這個問題時,當時進行了一番嘗試無解,找到了倆年前 @ash furrow 的方案,但原來的方案中無法同時處理 section 和 cell 的變化,我針對這個缺陷做了改進,但復雜而且難以使用;后來 @SixtyFrames?修正了這個缺陷,并且優化了原方案的邏輯,幾乎不需要改動就可使用。原方案的作者后來停止了維護該方案,并推薦了更優雅的解決方案:JSQDataSourcesKit,使用前提是 iOS 8和 Swift 2.0。?

我上周 fork 了這個庫,原本還打算給它貢獻點這個話題方面的代碼,結果發現它已經處理好了,而且代碼封裝得很好,我目前可寫不出這種水平的代碼。使用 Swift 有三個月左右了,越來越喜歡這個語言了,最近連 OC 都不會寫了。剛開始總會接觸到 protocol 的話題,但不是很理解,最近寫了幾個動畫,對 protocol 有點感受了,不過這個庫我目前看得有點吃力,不知道為何要那樣封裝,等三個月再看我的水平是否足夠。現成的解決方案是有了,但思路還是值得記錄的。

為 NSFetchedResultsController 對象提供 delegate 后,就能夠跟蹤數據的變化并更新視圖了, delegate 對象需要實現下面的方法:

-controllerWillChangeContent:

-controller:didChangeSection:atIndex:forChangeType:

-controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:

-controllerDidChangeContent:

當 NSManagedObjectContext 中對象發生了變化后,上面四個方法除了中間的兩個按照變化的情況來調用之外,基本上是按照順序來調用的。

對于 UICollectionView,來看看解決思路,ash furrow 指出問題的關鍵在于:

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 方法中搜集變化的內容,然后在最后的 didChangeContent 里使用 UICollectionView 的 performBatchUpdates: 方法來集中更新 UI。?

原方案沒有考慮到一些情況而無法處理同時發生了 section 和 cell 變化的情況,比如我的需求是批量操作多個 section 內的 cell,例如,選中不同 section 內的 cell,這包括了某個 section 內的全部 cell 以及其他 section 中部分 cell,然后新建一個 section,如下圖:

?忽略圖中的Bug

那么 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 是怎么判定操作的類型的,依據這個才好對操作進行分類和過濾。

操作類型有這么四種:

NSFetchedResultsChangeInsert?

NSFetchedResultsChangeDelete

NSFetchedResultsChangeMove

NSFetchedResultsChangeUpdate

NSFetchedResultsChangeMove 這種操作類型的通知里會包含目標的源位置 fromIndexPath 和新位置 toIndexPath,根據對這種操作類型的處理方式不同,有兩種方法來處理和過濾這些變化:

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 內的所有 cell 都會被移除,但不用分別刪除 section 和刪除 cell 這樣的重復操作,因此必須把該 section 內的所有 NSIndexPath 過濾掉。

上面的例子中沒有 NSIndexPath 被移除。

最后,對標記為 NSFetchedResultsChangeInsert 的操作進行過濾,去除那些目標 section 是新增 section 的 NSIndexPath。

上面的例子里沒有屬于 NSFetchedResultsChangeInsert 的 NSIndexPath,因此什么也不會移除。

接下來要對上面的過濾結果進行針對性的操作,更新 UI 要按照一定的順序來,這點在 performBatchUpdates:completion: 的文檔里有說明:delete 操作必須在 insert 之前,因為 insert 時的位置是相對于 delete 后再次計算的,這與我們之前看到的 log 恰好是反過來的。

另外,section 的變化會同時處理該 section 內所有 cell 的變化,所有只需要對 section 進行更新即可。

- 先處理 section 的變化:先刪除被標記的 section(可能有多個),然后添加被標記的 section(一般情況下只有一個)。

上面的例子 Section 2 被整體刪除,然后添加 Section 4。

- 然后處理不在 section 變化范圍內的漏網之魚,處理順序是,先處理刪除,然后是添加,移動和更新操作的順序無所謂,因為這兩個不影響整體的布局。

上面的例子里被判定為 delete 的三個目標將被刪除。

至此結束。

2. JSQDataSourcesKit

JSQDataSourcesKit 的方式與上面截然不同,簡直有種做奧賽題使用常規方法和特殊方法的區別:把 NSFetchedResultsChangeMove 拆分為先 Delete 再 Insert,這樣一來所有的操作只剩下 Delete 和 Insert(Update 不影響大局),因此只需要對搜集的變化進行分類而不需要過濾,而且在處理上不需要依賴特定的順序。在處理時,需要先處理 object 的變化,再處理 section 的變化。處理 object 的變化時,遇到 Move 操作時,先刪除原來的目標,在添加新的目標;而處理 section 的變化時,不處理 Move 操作。

簡潔有力!但老實說,除了拆分這個操作明白,后來對變化的處理不明白。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容