I am using CoreData
with a NSFetchResultController
to have data displayed in a UITableView
. I have one problem: the UITableView
changes the contentOffSet.y
when a new row is inserted/moved/deleted. When the user have scrolled to, for e.g. the middle, the UITableView
bounces when a new row is inserted.
This github link to a project which contains the minimum code to reproduce this behavior: https://github.com/Jasperav/FetchResultControllerGlitch (the code is down below as well)
This is showing the glitch. I am standing in the middle of my UITableView
and I am constantly seeing new rows being inserted, regardless of the current contentOffSet.y
.:
How to prevent from scrolling UITableView up when NSFetchedResultsController add new record?
not relevant since I explicitly set a rowHeight
and
estimatedRowHeight
.
Error: UITableView jump to top with UITableViewAutomaticDimension
tried this before the endUpdates
without luck
UITableView powered by FetchedResultsController with UITableViewAutomaticDimension - Cells move when table is reloaded
Same as first link, I have set the rowHeight
and
estimatedRowHeight
.
I also tried switch to performBatchUpdates
instead of begin/endUpdates
, that didn't worked out also.
The UITableView
just shouldn't move when inserting/deleting/moving rows when those rows aren't visible to the user. I expect something like this just should work out of the box.
This is what I eventually want (just a replication of the chat screen of WhatsApp):
UITableView
should animate the new inserted row and change the current contentOffSet.y
.If a UICollectionView
would be a better fit, that would be fine to.
I am trying to replicate the WhatsApp chat screen. I am not sure if they use NSFetchResultController, but besides that, the final goal is to provide them the exact user experience. So inserting, moving, deleting and updating cells should be done the way WhatsApp is doing it. So for a working example: go to WhatsApp, for a not-working example: download the project.
Code (ViewController.swift):
import CoreData
import UIKit
class ViewController: UIViewController, NSFetchedResultsControllerDelegate, UITableViewDataSource, UITableViewDelegate {
let tableView = MyTableView()
let resultController = ViewController.createResultController()
override func viewDidLoad() {
super.viewDidLoad()
// Initial cells
for i in 0...40 {
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = randomString(length: i + 1)
x.date = Date()
x.height = Float.random(in: 50...100)
}
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (_) in
let x = SomeEntity(context: CoreDataContext.persistentContainer.viewContext)
x.something = self.randomString(length: Int.random(in: 10...50))
x.date = Date()
x.height = Float.random(in: 50...100)
}
resultController.delegate = self
view.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
tableView.delegate = self
tableView.dataSource = self
tableView.estimatedRowHeight = 75
try! resultController.performFetch()
}
public func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
switch type {
case .insert:
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .delete:
tableView.deleteRows(at: [indexPath!], with: .automatic)
case .move:
tableView.deleteRows(at: [indexPath!], with: .automatic)
tableView.insertRows(at: [newIndexPath!], with: .automatic)
case .update:
tableView.moveRow(at: indexPath!, to: newIndexPath!)
}
}
func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.beginUpdates()
}
public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
tableView.endUpdates()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return resultController.fetchedObjects?.count ?? 0
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return CGFloat(resultController.object(at: indexPath).height)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyTableViewCell
cell.textLabel?.text = resultController.object(at: indexPath).something
return cell
}
private static func createResultController() -> NSFetchedResultsController<SomeEntity> {
let fetchRequest: NSFetchRequest<SomeEntity> = SomeEntity.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false)]
return NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: CoreDataContext.persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil)
}
func randomString(length: Int) -> String {
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return String((0...length-1).map{ _ in letters.randomElement()! })
}
}
class MyTableView: UITableView {
init() {
super.init(frame: .zero, style: .plain)
register(MyTableViewCell.self, forCellReuseIdentifier: "cell")
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class MyTableViewCell: UITableViewCell {
}
class CoreDataContext {
static let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "FetchViewControllerGlitch")
container.loadPersistentStores(completionHandler: { (nsPersistentStoreDescription, error) in
guard let error = error else {
return
}
fatalError(error.localizedDescription)
})
return container
}()
}
let lastScrollOffset = tableView.contentOffset;
tableView.beginUpdates();
tableView.insertRows(at: [newIndexPath!], with: .automatic);
tableView.endUpdates();
tableView.layer.removeAllAnimations();
tableView.setContentOffset(lastScrollOffset, animated: false);
Do the best you can establishing estimated heights for all of your table cell types. Even if heights are somewhat dynamic this helps the UITableView.
Save your scroll position and after updating your tableView and making a call to endUpdates() reset the content offset.
You can also check this tutorial
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With