Glitch
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.
Reproduction project
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.:
Similar questions
How to prevent from scrolling UITableView up when NSFetchedResultsController add new record? not relevant since I explicitly set a
rowHeightandestimatedRowHeight.Error: UITableView jump to top with UITableViewAutomaticDimension tried this before the
endUpdateswithout luckUITableView powered by FetchedResultsController with UITableViewAutomaticDimension - Cells move when table is reloaded Same as first link, I have set the
rowHeightandestimatedRowHeight.
Concerns
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.
Final goal
This is what I eventually want (just a replication of the chat screen of WhatsApp):
- When the user is completely scrolled to the top (for WhatsApp this is the bottom) where the new rows are being inserted, the 
UITableViewshould animate the new inserted row and change the currentcontentOffSet.y. - When the user isn't completely scrolled to the top (or bottom, depending where the new rows are being inserted) the cells the user is seeing should not bounce around when a new row is inserted. This is really bad for the user experience of the application.
 - It should work for dynamic height cells.
 - I also see this behavior when moving/deleting cells. Is there any easy fix for all glitches here?
 
If a UICollectionView would be a better fit, that would be fine to.
Use case
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.
Copy paste code
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
    }()
}
