Bartłomiej Semańczyk Bartłomiej Semańczyk - 3 months ago 17
Swift Question

How to prevent from scrolling UITableView up when NSFetchedResultsController add new record?

My

UITableView
is scrolling up once I add a new comment to CoreData. NSFetchedResultsController knows about it, puts this comment at the bottom of the table, and scroll table to top. Is it normal?

My example:

Just before I add a comment:

enter image description here

Just after I add a comment (not expected behavior):

enter image description here

It should be just like this (after I swipe it by hand manually):

enter image description here

This may be connected with following situation:


  1. This is sort descriptor of my
    NSFetchedResultsController
    :

    NSSortDescriptor(key: "createdAt", ascending: false) //from the latest to the oldest

  2. But I need to display my comments for reversed index paths (the latest are at the very bottom). In other words, everywhere when I need to use
    indexPath
    I use:

    private func reversedIndexPathForIndexPath(indexPath: NSIndexPath) -> NSIndexPath {
    return NSIndexPath(forRow: fetchedResultsController.fetchedObjects!.count - indexPath.row - 1, inSection: 0)
    }



NSFetchedResultsControllerDelegate
:

//MARK: - NSFetchedResultsControllerDelegate

func controllerWillChangeContent(controller: NSFetchedResultsController) {
self.tableView.beginUpdates()
}

func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {

switch type {
case .Insert:
if let newIndexPath = newIndexPath {
tableView.insertRowsAtIndexPaths([reversedIndexPathForIndexPath(newIndexPath)], withRowAnimation: .Fade)
}
case .Delete:
if let indexPath = indexPath {
tableView.deleteRowsAtIndexPaths([reversedIndexPathForIndexPath(indexPath)], withRowAnimation: .Fade)
}
case .Update:
if let indexPath = indexPath {
tableView.reloadRowsAtIndexPaths([reversedIndexPathForIndexPath(indexPath)], withRowAnimation: .Fade)
}
case .Move:
if let indexPath = indexPath, let newIndexPath = newIndexPath {
tableView.deleteRowsAtIndexPaths([reversedIndexPathForIndexPath(indexPath)], withRowAnimation: .Fade)
tableView.insertRowsAtIndexPaths([reversedIndexPathForIndexPath(newIndexPath)], withRowAnimation: .Fade)
}
}
}

func controllerDidChangeContent(controller: NSFetchedResultsController) {
tableView.endUpdates()
}


Is it connected to my problem?

Answer

Edited answer:

I was able to reproduce this glitch with the following steps:

  1. UITableView with its rowHeight set to UITableViewAutomaticDimension and non-zero estimatedRowHeight.
  2. Table scrolled to its very bottom, with the last cell fully visible at the bottom margin.
  3. Inserting the new cell below the last one results in visual shift of the cells a few points down (in my setup it differs from 5 to 40).

The source of this behavior needs additional investigation.

For now the only solution is not to use UITableViewAutomaticDimension and return row heights from tableView:heightForRowAtIndexPath:.

By the way, implementation of inverted index paths has one significant flaw. When FRC calls it's delegate method controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:, all deletion index paths belongs to pre-change data set, and all insertion index paths - to post-change one. When you are in-beetween controllerWillChangeContent: and controllerDidChangeContent:, fetchedResultsController.fetchedObjects will contain post-change data set, i suppose (needs to be checked, though).