Search code examples
uitableviewswiftcore-datansfetchedresultscontrollernssortdescriptor

Inserting rows with multiple NSFetchedResultsController's


I am downloading information from the internet and using the data to create entities in Core Data. I am trying to sort the entities (The entities are TV Shows, the data is from Trakt) by the airDate attribute of a TVEpisode entity that has a relationship to the TVShow entity. The TVShow entity only has this relationship to the show if the show data has an episode that is airing at a future date from the current time.

So the way I want to sort the data is: Top: Shows that have a upcomingEpisode relationship, sorted by the airDate attribute of the upcomingEpisode, ordered ascendingly. Middle: Shows that have no upcomingEpisode relationship but will be returning. Bottom: Shows that have no upcomingEpisode relationship and that are ended/cancelled

Here are the issues I am running into getting this to work.

Issue 1: Using 1 NSFetchedResultsController

let fetchRequest = NSFetchRequest(entityName: "TVShow")
    let airDateSort = NSSortDescriptor(key: "upcomingEpisode.airDate", ascending: true)
    let titleSort = NSSortDescriptor(key: "title", ascending: true)
    fetchRequest.sortDescriptors = [airDateSort, titleSort];

    upcomingShowsResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: "upcomingShows")
    upcomingShowsResultsController.delegate = self;

    var error: NSError? = nil
    if (!upcomingShowsResultsController.performFetch(&error)) {
        println("Error: \(error?.localizedDescription)")
    }

Using this NSFetchedResultsController will put all TVShow entities with no upcomingEpisode relationship on top, sorted all by title, I need the dead shows sorted by title on the very bottom and returning shows sorted by title in the middle.

Issue 2: Using multiple NSFetchedResultsController's

func setupUpcomingShowsFetchedResultsController() {
    let fetchRequest = NSFetchRequest(entityName: "TVShow")
    let airDateSort = NSSortDescriptor(key: "upcomingEpisode.airDate", ascending: true)
    let titleSort = NSSortDescriptor(key: "title", ascending: true)
    fetchRequest.sortDescriptors = [airDateSort, titleSort];

    let predicate = NSPredicate(format: "upcomingEpisode != nil")
    fetchRequest.predicate = predicate

    upcomingShowsResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: "upcomingShows")
    upcomingShowsResultsController.delegate = self;

    var error: NSError? = nil
    if (!upcomingShowsResultsController.performFetch(&error)) {
        println("Error: \(error?.localizedDescription)")
    }
}

func setupReturningShowsFetchedResultsController() {
    let fetchRequest = NSFetchRequest(entityName: "TVShow")
    let titleSort = NSSortDescriptor(key: "title", ascending: true)
    fetchRequest.sortDescriptors = [titleSort];

    let predicate = NSPredicate(format: "status == 'returning series'")
    let predicate2 = NSPredicate(format: "upcomingEpisode == nil")
    fetchRequest.predicate = NSCompoundPredicate.andPredicateWithSubpredicates([predicate!, predicate2!])

    returningShowsResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
    returningShowsResultsController.delegate = self;

    var error: NSError? = nil
    if (!returningShowsResultsController.performFetch(&error)) {
        println("Error: \(error?.localizedDescription)")
    }
}

func setupDeadShowsFetchedResultsController() {
    let fetchRequest = NSFetchRequest(entityName: "TVShow")
    let titleSort = NSSortDescriptor(key: "title", ascending: true)
    fetchRequest.sortDescriptors = [titleSort]

    let endedShowsPredicate = NSPredicate(format: "status == 'ended'")
    fetchRequest.predicate = endedShowsPredicate

    deadShowsResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
    deadShowsResultsController.delegate = self;

    var deadShowsError: NSError? = nil
    if (!deadShowsResultsController.performFetch(&deadShowsError)) {
        println("Error: \(deadShowsError?.localizedDescription)")
    }
}

These work for what I want, but only when the data is already downloaded and in Core Data. When the app first launches and downloads the data it crashes every time because the number of rows in a section are not the same as what the table is expecting. I did manipulate the index paths that the NSFetchedResultsControllerDelegate gives in the didChangeObject function, and I printed out index's that are being inserted. The count that I did in any section was equal to how many the table view says it was expecting but it throws an error every time. This is how I am handling the method for multiple NSFetchedResultsController's

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
        let section = sectionOfFetchedResultsController(controller)
        let indexPathsComputed = [NSIndexPath(forRow: indexPath?.row ?? 0, inSection: section)]
        let newIndexPathsComputed = [NSIndexPath(forRow: newIndexPath?.row ?? 0, inSection: section)]

        dispatch_async(dispatch_get_main_queue(), { () -> Void in
            switch type {
            case NSFetchedResultsChangeType.Insert:
                self.tableView.insertRowsAtIndexPaths(newIndexPathsComputed, withRowAnimation: .Automatic)
            case NSFetchedResultsChangeType.Delete:
                self.tableView.deleteRowsAtIndexPaths(indexPathsComputed, withRowAnimation: .Automatic)
            case NSFetchedResultsChangeType.Move:
                self.tableView.deleteRowsAtIndexPaths(indexPathsComputed, withRowAnimation: .Automatic)
                self.tableView.insertRowsAtIndexPaths(newIndexPathsComputed, withRowAnimation: .Automatic)
            case NSFetchedResultsChangeType.Update:
                if let index = indexPathsComputed[0] {
                    if let cell = self.tableView.cellForRowAtIndexPath(index) as? ShowTableViewCell {
                        self.configureCell(cell, indexPath: index)
                    }
                }
                else {
                    println("No cell at index path")
                }
            }
        })
    }

If the crashes could be fixed, this would be the best way to achieve what I want to do.

Issue 3: Using multiple Array's

func reloadShowsArray() {
    let fetchRequest = NSFetchRequest(entityName: "TVShow")
    let airDateSort = NSSortDescriptor(key: "upcomingEpisode.airDate", ascending: true)
    let titleSort = NSSortDescriptor(key: "title", ascending: true)
    fetchRequest.sortDescriptors = [airDateSort, titleSort];

    let predicate = NSPredicate(format: "upcomingEpisode != nil")
    fetchRequest.predicate = predicate

    var error: NSError?
    showsArray = coreDataStack.context.executeFetchRequest(fetchRequest, error: &error) as [TVShow]
    if let error = error {
        println(error)
    }
}

func reloadDeadShows() {
        let fetchRequest = NSFetchRequest(entityName: "TVShow")
        let titleSort = NSSortDescriptor(key: "title", ascending: true)
        fetchRequest.sortDescriptors = [titleSort]

        let endedShowsPredicate = NSPredicate(format: "status == 'ended'")
        fetchRequest.predicate = endedShowsPredicate

        var error: NSError?
        deadShows = coreDataStack.context.executeFetchRequest(fetchRequest, error: &error) as [TVShow]
        if let error = error {
            println(error)
        }
    }

This solves the crashing and works after the data is downloaded and while the data is being downloaded. But when using this, I have to call self.tableView.reloadData() when the data is downloaded, and the entities just pop into the table view with no animation, and I really want the animations from insertRowsAtIndexPaths because it looks better and is a better experience. I tried calling reloadShowsArray() and then using the find() function with the entity to get the index so I could use insertRowsAtIndexPaths, but it returns nil every time for the index, even though the entity was saved with the context before that. Also the cells will not get automatically reloaded or moved around like with NSFetchedResultsController

So what is the best way to handle this, and how can I get the desired sorting with the animations?


Solution

  • As per comments, I suspect the three-FRC method causes problems because one FRC calls controller:didChangeContent (which triggers tableView.endUpdates) while another FRC is still processing updates. To overcome this, implement a counter which is incremented in controller:willChangeContent and decremented in controller:didChangeContent. The tableView beginUpdates should only be called if the counter is zero, and endUpdates only when the counter returns to zero. That way, the endUpdates will only be called when all three FRCs have completed processing their updates.

    If possible, I would also avoid the dispatch_async, since it could result in the table updates occurring outside the beginUpdates/endUpdates cycle.