UITableView insert row animation inconsistent with self-sizing cells

Viewed 145

I created a live playground below to illustrate the question. When using self-sizing table cells, the insert row animation (bottom-up) is executed differently depending on the size of the last cell. If the last cell is taller than the cell that is about to be inserted, the animation begins too high and thus animates downward (not upward). What is going on here?

import UIKit
import PlaygroundSupport

class Live: UIViewController {
    private var entries = ["a\na\na", "b\nb\nb", "c\nc\nc", "d\nd\nd", "e\ne\ne", "f"]
    private let newEntries = ["correct\nanimation\nhere", "correct\nanimation\nhere", "incorrect animation", "correct\nanimation\nhere", "incorrect animation", "correct\nanimation\nhere"]
    private let tableView = UITableView()
    
    override func loadView() {
        view = UIView()
        view.backgroundColor = .blue
        
        tableView.dataSource = self
        tableView.delegate = self
        tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 64, right: 0)
        tableView.backgroundColor = .purple
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.register(EntryCell.self, forCellReuseIdentifier: EntryCell.reuseId)
        view.addSubview(tableView)
        tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        tableView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
        tableView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        for (n, new) in newEntries.enumerated() {
            DispatchQueue.main.asyncAfter(deadline: .now() + Double(n*2)) {
                self.entries.append(new)
                
                self.tableView.performBatchUpdates({
                    self.tableView.insertRows(at: [IndexPath(row: self.entries.count - 1, section: 0)], with: .bottom)
                }) { (finished) in
                    if finished {
                        DispatchQueue.main.async {
                            self.tableView.scrollToRow(at: IndexPath(row: self.entries.count - 1, section: 0), at: .bottom, animated: true)
                        }
                    }
                }
            }
        }
    }
}

extension Live: UITableViewDataSource {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return entries.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: EntryCell.reuseId, for: indexPath) as! EntryCell
        cell.entryLabel.text = entries[indexPath.row]
        return cell
    }
}

extension Live: UITableViewDelegate {
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        return UIView()
    }

    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        return UIView()
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 0
    }
    
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 0
    }
}

extension Live {
    private class EntryCell: UITableViewCell {
        static let reuseId = "cell"
        let entryLabel = UILabel()

        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
            isOpaque = true
            selectionStyle = .none
            backgroundColor = .brown
            
            entryLabel.textColor = .white
            entryLabel.numberOfLines = 0
            entryLabel.translatesAutoresizingMaskIntoConstraints = false
            addSubview(entryLabel)
            entryLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16).isActive = true
            entryLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16).isActive = true
            entryLabel.widthAnchor.constraint(equalTo: widthAnchor, constant: -32).isActive = true
            entryLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16).isActive = true
        }
        
        required init?(coder aDecoder: NSCoder) {
            fatalError()
        }
    }
}

PlaygroundPage.current.liveView = Live()
1 Answers

Just changed insert rows from bottom to automatic, it will load cells as per scroll.

self.tableView.insertRows(at: [IndexPath(row: self.entries.count - 1, section: 0)], with: .automatic)
Related