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?
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.