Using UICollectionView with CoreData and NSFetchedResultsController

9.7k views Asked by At

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!

2

There are 2 answers

1
DogCoffee On BEST ANSWER

Sounds like an issue I had a while ago, when I was using objective C.

Check out

UICollectionView Assertion failure

I had to do a work around to get my FRC to work with the collection view.

Seems like you found the same git as me, here is the fix that solves the collection view issue.

https://github.com/AshFurrow/UICollectionView-NSFetchedResultsController/issues/13

2
Alex Shubin On

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

}