iOS
UITableView
UITableViewCell
UITableViewAutomaticDimension
jerky scrolling

Jerky Scrolling After Updating UITableViewCell in place with UITableViewAutomaticDimension

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Jerky scrolling in UITableView with self-sizing cells (UITableView.automaticDimension) happens when the table view's estimated row height does not match the actual height, causing the table to recalculate content offsets during scrolling. The primary fix is to cache cell heights after they are displayed and return the cached value from estimatedHeightForRowAt. This prevents the table view from guessing heights, which eliminates the jumping and stuttering that occurs when estimated and actual heights differ significantly.

The Problem

swift
1// Standard self-sizing cell setup
2tableView.rowHeight = UITableView.automaticDimension
3tableView.estimatedRowHeight = 44  // This default estimate causes issues
4
5// When cells have varying heights (e.g., 44, 200, 80, 300),
6// the table view uses the estimate (44) for off-screen cells.
7// As cells scroll into view, the actual height replaces the estimate,
8// causing the content offset to shift — the visible content "jumps."

The table view uses estimated heights to calculate scroll indicators and content size. When the actual height differs from the estimate, the table adjusts its content offset, producing visible jerks.

Fix 1: Cache Cell Heights

swift
1class SmoothTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
2
3    var tableView: UITableView!
4    var cellHeightCache: [IndexPath: CGFloat] = [:]
5
6    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
7        // Cache the actual height after the cell is laid out
8        cellHeightCache[indexPath] = cell.bounds.height
9    }
10
11    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
12        // Return cached height if available, otherwise use a reasonable default
13        return cellHeightCache[indexPath] ?? 100
14    }
15}

This is the most effective fix. After a cell is displayed, its actual height is cached. On subsequent layout passes, the cached value is returned as the estimate, preventing any height mismatch.

Fix 2: Better Default Estimates

swift
1// Instead of a single estimatedRowHeight for all cells,
2// provide per-section or per-content-type estimates
3
4func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
5    switch indexPath.section {
6    case 0: return 80   // Header cells
7    case 1: return 200  // Image cells
8    case 2: return 60   // Text-only cells
9    default: return 100
10    }
11}

If caching is not feasible, providing estimates that closely match actual heights reduces the severity of jumps.

Fix 3: Use beginUpdates/endUpdates for In-Place Updates

swift
1// WRONG: reloadRows causes height recalculation and jumps
2tableView.reloadRows(at: [indexPath], with: .automatic)
3
4// BETTER: update the cell directly without reloading
5if let cell = tableView.cellForRow(at: indexPath) as? CustomCell {
6    cell.configure(with: updatedData)
7
8    // Force layout recalculation without animation
9    tableView.beginUpdates()
10    tableView.endUpdates()
11}

beginUpdates()/endUpdates() tells the table view to recalculate heights for visible cells without reloading them. This avoids the jarring reload animation.

Fix 4: Disable Automatic Prefetching

swift
// iOS 15+ prefetching can cause layout issues with dynamic cells
tableView.isPrefetchingEnabled = false

Prefetching calculates cell sizes for off-screen rows using estimated heights, which can trigger additional content offset adjustments when the actual sizes differ.

Fix 5: Use performBatchUpdates Instead

swift
1// Modern approach (iOS 11+)
2tableView.performBatchUpdates({
3    // Update your data model here
4    dataSource[indexPath.row] = updatedItem
5
6    // Optionally reload the specific row
7    tableView.reloadRows(at: [indexPath], with: .none)  // .none avoids animation
8}, completion: nil)

performBatchUpdates groups insertions, deletions, and reloads into a single animation pass, reducing layout thrashing.

Fix 6: Prevent Content Offset Jumps

swift
1// Save and restore content offset around updates
2let offset = tableView.contentOffset
3
4tableView.reloadRows(at: [indexPath], with: .none)
5
6// Restore offset after reload
7tableView.layoutIfNeeded()
8tableView.setContentOffset(offset, animated: false)

This brute-force approach saves the scroll position before the update and restores it after, preventing any visible jump.

Comprehensive Solution

swift
1class SmoothTableViewController: UIViewController {
2
3    @IBOutlet weak var tableView: UITableView!
4    private var heightCache: [IndexPath: CGFloat] = [:]
5    var items: [CellModel] = []
6
7    override func viewDidLoad() {
8        super.viewDidLoad()
9        tableView.rowHeight = UITableView.automaticDimension
10        tableView.estimatedRowHeight = UITableView.automaticDimension
11        tableView.delegate = self
12        tableView.dataSource = self
13    }
14
15    func updateCell(at indexPath: IndexPath, with newData: CellModel) {
16        items[indexPath.row] = newData
17
18        // Invalidate cached height for this row
19        heightCache.removeValue(forKey: indexPath)
20
21        if let cell = tableView.cellForRow(at: indexPath) as? DynamicCell {
22            // Update visible cell in place
23            cell.configure(with: newData)
24            tableView.beginUpdates()
25            tableView.endUpdates()
26        } else {
27            // Cell is off-screen — reload it
28            tableView.reloadRows(at: [indexPath], with: .none)
29        }
30    }
31}
32
33extension SmoothTableViewController: UITableViewDelegate {
34
35    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
36        heightCache[indexPath] = cell.bounds.height
37    }
38
39    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
40        return heightCache[indexPath] ?? 100
41    }
42}

Common Pitfalls

  • Using a single estimatedRowHeight for all cells: A single estimate like 44 causes massive jumps when actual heights vary (e.g., 200-point image cells). Implement estimatedHeightForRowAt with per-cell or cached heights for accurate estimates.
  • Calling reloadData() instead of targeted updates: reloadData() invalidates all cached heights and recalculates the entire table, causing visible jumps. Use reloadRows(at:with:) or beginUpdates/endUpdates to update specific cells.
  • Forgetting to invalidate the height cache after data changes: If cell content changes (longer text, added image), the cached height is stale. Remove the cache entry for the updated row before triggering a layout pass.
  • Using .automatic animation for in-place updates: The .automatic animation style adds a fade/slide animation to reloaded cells, which can look like a flicker. Use .none when updating cell content in place to avoid visual artifacts.
  • Not setting constraints correctly in self-sizing cells: Self-sizing cells require an unbroken chain of constraints from the cell's contentView top to bottom. Missing or ambiguous constraints cause the auto-layout engine to compute incorrect heights, leading to jumps when the correct height is determined later.

Summary

  • Cache cell heights in willDisplay and return them from estimatedHeightForRowAt to prevent height mismatch jumps
  • Use beginUpdates()/endUpdates() to update visible cells without reloading
  • Provide per-section or per-type height estimates instead of a single estimatedRowHeight
  • Use .none animation when reloading rows in place to avoid visual flickering
  • Invalidate cached heights when cell content changes
  • Ensure self-sizing cells have an unbroken top-to-bottom constraint chain in the contentView

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.