Reputation: 785
UITableView
unexpectedly bounces with beginUpdates()
/ endUpdates()
/ performBatchUpdates()
using NSFetchedResultsController
and CoreData when the number of rows fill the view.
It's pretty simple to reproduce.
- Create a new project from the Master-Detail App Template (with CoreData).
- In the storyboard, remove the "showDetail" segue. (we don't need the detail view)
- In MasterViewController, replace segue func prepare()
with :
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let event = fetchedResultsController.object(at: indexPath)
let timestamp = event.timestamp
event.timestamp = timestamp // The idea is to simply update the Event entity.
}
Launch the app (in iOS devices or Simulators), and add enough rows to fill the view (in iPhone SE, it 11 rows). Scroll down the view, and select any row. The view WILL rapidly BOUNCE up and down. Is that a bug, or is there an issue with the code ?
Upvotes: 4
Views: 1255
Reputation: 785
A more refined solution is
lazy var sectionChanges = [() -> Void]()
lazy var objectChanges = [() -> Void]()
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller == self._fetchedResultsController else { return }
self.sectionChanges.removeAll()
self.objectChanges.removeAll()
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
guard controller == self._fetchedResultsController else { return }
let sections = IndexSet(integer: sectionIndex)
self.sectionChanges.append { [unowned self] in
switch type {
case .insert: self.tableView.insertSections(sections, with: .fade)
case .delete: self.tableView.deleteSections(sections, with: .fade)
default: break
}
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
guard controller == self._fetchedResultsController else { return }
switch type {
case .insert:
if let verifiedNewIndexPath = newIndexPath {
self.objectChanges.append { [unowned self] in
self.tableView.insertRows(at: [verifiedNewIndexPath], with: .fade)
}
}
case .delete:
if let verifiedIndexPath = indexPath {
self.objectChanges.append { [unowned self] in
self.tableView.deleteRows(at: [verifiedIndexPath], with: .fade)
}
}
case .update:
if let verifiedIndexPath = indexPath, let event = anObject as? Event, let cell = self.tableView.cellForRow(at: verifiedIndexPath) {
self.configureCell(cell, withEvent: event)
}
case .move:
if let verifiedIndexPath = indexPath, let verifiedNewIndexPath = newIndexPath, let event = anObject as? Event, let cell = self.tableView.cellForRow(at: verifiedIndexPath) {
self.configureCell(cell, withEvent: event)
self.objectChanges.append { [unowned self] in
self.tableView.moveRow(at: verifiedIndexPath, to: verifiedNewIndexPath)
}
}
default: break
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard controller == self._fetchedResultsController else { return }
guard self.objectChanges.count > 0 || self.sectionChanges.count > 0 else { return }
self.tableView.performBatchUpdates({[weak self] in
self?.objectChanges.forEach { $0() }
self?.sectionChanges.forEach { $0() }
}) { (finished) in
// here I check if the tableView is empty. If so, I usually add a label saying "no item, click add button to add items."
// If not, then I remove this label.
}
}
Upvotes: 1
Reputation: 2169
I noticed the similar (duplicate?) question at UITableView unexpectedly bounces with beginUpdates()/endUpdates()/performBatchUpdates()
I added an answer there about using the estimatedHeightFor...
methods of the table view. Implementing these methods to return a positive number fixes the odd bounce problem during table view batch updates.
Upvotes: 0
Reputation: 785
Ok, I might have found a solution, please tell me guys what you think.
The idea would be to process insert/delete/move
in performBatchUpdates
and leave update
out of it.
So I've created this enum and property:
enum FetchedResultsChange<Object> {
case insert(IndexPath)
case delete(IndexPath)
case move(IndexPath, IndexPath, Object)
}
var fetchedResultsChanges: [FetchedResultsChange<Event>] = []
And controllerWillChangeContent
becomes empty:
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {}
didChange
becomes:
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
self.fetchedResultsChanges.append(.insert(newIndexPath!))
case .delete:
self.fetchedResultsChanges.append(.delete(indexPath!))
case .update:
configureCell(tableView.cellForRow(at: indexPath!)!, withEvent: anObject as! Event) // So this stays untouched.
case .move:
self.fetchedResultsChanges.append(.move(indexPath!, newIndexPath!, anObject as! Event))
}
}
And controllerDidChangeContent
becomes:
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
guard self.fetchedResultsChanges.count > 0 else { return }
tableView.performBatchUpdates({
repeat {
let change = self.fetchedResultsChanges.removeFirst()
switch change {
case .insert(let newIndexPath):
tableView.insertRows(at: [newIndexPath], with: .fade)
case .delete(let indexPath):
tableView.deleteRows(at: [indexPath], with: .fade)
case .move(let indexPath, let newIndexPath, let event):
configureCell(tableView.cellForRow(at: indexPath)!, withEvent: event)
tableView.moveRow(at: indexPath, to: newIndexPath)
}
} while self.fetchedResultsChanges.count > 0
}, completion: nil)
}
So what do you think ?
Upvotes: 3
Reputation: 397
This may help -
UIView.performWithoutAnimation {
self.tableView?.beginUpdates()
let contentOffset = self.tableView?.contentOffset
self.tableView?.reloadRows(at: [IndexPath(row: j, section: 0)], with: .automatic)
self.tableView?.setContentOffset(contentOffset!, animated: false)
self.tableView?.endUpdates()
}
Upvotes: -2