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()