I recently started another project exploring Swift a little bit. I want to implement a Collection View using a NSFetchedResultsController to get the data out of my CoreData database. I wanted to use the example from https://github.com/AshFurrow/UICollectionView-NSFetchedResultsController and implement something similar in Swift. I don't need any of the move events so I implemented the following:
First of all I created a class to save changes made:
class ChangeItem{
var index:NSIndexPath
var type:NSFetchedResultsChangeType
init(index: NSIndexPath, type: NSFetchedResultsChangeType){
self.index = index
self.type = type
}
}
within my collection view controller I use two arrays to save the changes temporarily
var sectionChanges:[ChangeItem] = []
var objectChanges:[ChangeItem] = []
then I wait for my FetchedResultsController to change something after changes on the CoreData database were made
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?,forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
var item:ChangeItem?
if(type == NSFetchedResultsChangeType.Insert){
item = ChangeItem(index: newIndexPath!, type: type)
}else if(type == NSFetchedResultsChangeType.Delete){
item = ChangeItem(index: indexPath!, type: type)
}else if(type == NSFetchedResultsChangeType.Update){
item = ChangeItem(index: indexPath!, type: type)
}
if(item != nil){
self.objectChanges.append(item!)
}
}
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
var item:ChangeItem?
if(type == NSFetchedResultsChangeType.Insert || type == NSFetchedResultsChangeType.Delete || type == NSFetchedResultsChangeType.Update){
item = ChangeItem(index: NSIndexPath(forRow: 0, inSection: sectionIndex), type: type)
}
if(item != nil){
self.sectionChanges.append(item!)
}
}
After all changes are applied the FetchedResultsController will perform the didChangeContent method
func controllerDidChangeContent(controller: NSFetchedResultsController) {
println("something happened")
self.collectionView!.performBatchUpdates({ () -> Void in
while(self.sectionChanges.count > 0 || self.objectChanges.count > 0){
self.insertSections()
self.insertItems()
self.updateItems()
let delips = self.deleteSections()
self.deleteItems(delips)
}
}, completion: { (done) -> Void in
if(done){
println("done")
self.collectionView.reloadData()
}else{
println("not done")
self.collectionView.reloadData()
}
if(self.fetchedResultsController.sections != nil && self.fetchedResultsController.sections?.count > 0){
println("number of items in first section \(self.fetchedResultsController.sections![0].count)")
}
})
self.deselectAll()
self.collectionView.reloadData()
if(self.fetchedResultsController.sections != nil && self.fetchedResultsController.sections!.count > 0){
for i in 1 ..< self.fetchedResultsController.sections!.count{
if(self.fetchedResultsController.sections!.count > 0){
println(self.fetchedResultsController.sections![i].numberOfObjects )
}
}
}
}
which then again calls the following methods
private func deleteSections()->NSIndexSet{
var deletes = self.sectionChanges.filter({ (item) -> Bool in
if(item.type == NSFetchedResultsChangeType.Delete){
let index = (self.sectionChanges as NSArray).indexOfObject(item)
self.sectionChanges.removeAtIndex(index)
return true
}
return false
}) as [ChangeItem]
var indexSet:NSMutableIndexSet = NSMutableIndexSet()
for del in deletes{
indexSet.addIndex(del.index.section)
}
if(indexSet.count > 0 && self.collectionView.numberOfSections() > 0){
self.collectionView.deleteSections(indexSet)
}
return indexSet
}
private func insertSections(){
var inserts = self.sectionChanges.filter({ (item) -> Bool in
if(item.type == NSFetchedResultsChangeType.Insert){
let index = (self.sectionChanges as NSArray).indexOfObject(item)
self.sectionChanges.removeAtIndex(index)
return true
}
return false
}) as [ChangeItem]
var indexSet:NSMutableIndexSet = NSMutableIndexSet()
for ins in inserts{
indexSet.addIndex(ins.index.section)
}
if(indexSet.count > 0){
println("Adding \(indexSet.count) section")
println(indexSet)
self.collectionView.insertSections(indexSet)
self.collectionView.reloadSections(indexSet)
}
}
private func deleteItems(deletedSections:NSIndexSet?){
var deletes = self.objectChanges.filter({ (item) -> Bool in
if(item.type == NSFetchedResultsChangeType.Delete){
let index = (self.objectChanges as NSArray).indexOfObject(item)
self.objectChanges.removeAtIndex(index)
return true
}
return false
}) as [ChangeItem]
var indexPaths:[NSIndexPath] = []
for del in deletes{
if(del.index.section < self.fetchedResultsController.sections?.count){
if(deletedSections == nil || deletedSections!.containsIndex(del.index.section)){
indexPaths.append(del.index)
}
}
}
/*indexPaths = indexPaths.sorted({ (a, b) -> Bool in
if(a.section >= b.section && a.row >= b.row){
return true
}
return false
})
println(indexPaths)*/
//self.collectionView.numberOfItemsInSection(0)
if(indexPaths.count > 0){
println("deleting \(indexPaths.count) items")
self.collectionView.deleteItemsAtIndexPaths(indexPaths)
}
}
private func updateItems(){
var updates = self.objectChanges.filter({ (item) -> Bool in
if(item.type == NSFetchedResultsChangeType.Update){
let index = (self.objectChanges as NSArray).indexOfObject(item)
self.objectChanges.removeAtIndex(index)
return true
}
return false
}) as [ChangeItem]
var indexPaths:[NSIndexPath] = []
for update in updates{
indexPaths.append(update.index)
}
if(indexPaths.count > 0 ){
println("did update on \(indexPaths.count) items")
self.collectionView.reloadItemsAtIndexPaths(indexPaths)
}
}
private func insertItems(){
var inserts = self.objectChanges.filter({ (item) -> Bool in
if(item.type == NSFetchedResultsChangeType.Insert){
let index = (self.objectChanges as NSArray).indexOfObject(item)
self.objectChanges.removeAtIndex(index)
return true
}
return false
}) as [ChangeItem]
var indexPaths:[NSIndexPath] = []
for ins in inserts{
indexPaths.append(ins.index)
}
if(indexPaths.count > 0 && self.numberOfSectionsInCollectionView(self.collectionView) > 0){
println("Adding \(indexPaths.count) items to collection view with \(self.collectionView.numberOfItemsInSection(0))")
self.collectionView.insertItemsAtIndexPaths(indexPaths)
println("Did add items")
}
}
Deleting multiple items and sections at a time works just fine (by now), but inserting a section does not work. As you can see I implemented a output within the insertSections method. The number of new sections in my test scenario is 1 and I get the right NSIndexSet with 1 item:(0). But in the insertItems method I try to call numberOfObjects on section 0 and get the following Error
*** Assertion failure in -[UICollectionViewData numberOfItemsInSection:], /SourceCache/UIKit_Sim/UIKit3347.44/UICollectionViewData.m:5942015-06-19:00:53:01.966 Grocli[42533:1547866] CoreData: error: Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. request for number of items in section 0 when there are only 0 sections in the collection view with userInfo (null)
Thanks in advance for reading that long article and helping me out!
Here's my idea of implementation of FRC delegate methods.
In this case for UICollectionViewController subclass:
Swift 3
import UIKit
import CoreData
class FetchedResultsCollectionViewController: UICollectionViewController, NSFetchedResultsControllerDelegate {
private var sectionChanges = [(type: NSFetchedResultsChangeType, sectionIndex: Int)]()
private var itemChanges = [(type: NSFetchedResultsChangeType, indexPath: IndexPath?, newIndexPath: IndexPath?)]()
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
sectionChanges.append((type, sectionIndex))
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
{
itemChanges.append((type, indexPath, newIndexPath))
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
collectionView?.performBatchUpdates({
for change in self.sectionChanges {
switch change.type {
case .insert: self.collectionView?.insertSections([change.sectionIndex])
case .delete: self.collectionView?.deleteSections([change.sectionIndex])
default: break
}
}
for change in self.itemChanges {
switch change.type {
case .insert: self.collectionView?.insertItems(at: [change.newIndexPath!])
case .delete: self.collectionView?.deleteItems(at: [change.indexPath!])
case .update: self.collectionView?.reloadItems(at: [change.indexPath!])
case .move:
self.collectionView?.deleteItems(at: [change.indexPath!])
self.collectionView?.insertItems(at: [change.newIndexPath!])
}
}
}, completion: { finished in
self.sectionChanges.removeAll()
self.itemChanges.removeAll()
})
}
}