UITableView
iOS Development
Dynamic Cell Heights
Scrolling Performance
reloadData

reloadData of UITableView with Dynamic cell heights causes jumpy scrolling

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Calling reloadData() on a UITableView with self-sizing cells (dynamic heights) frequently causes the scroll position to jump unpredictably. This happens because the table view discards all cached cell heights and re-estimates them, causing the content offset to shift. This is one of the most common UITableView performance issues in iOS development.

Why It Happens

When you call reloadData(), the table view:

  1. Discards all previously calculated cell heights
  2. Uses estimatedRowHeight for all cells that are not currently visible
  3. Recalculates heights only for visible cells using Auto Layout
  4. Adjusts contentSize and contentOffset based on the new estimates

If the estimated heights differ significantly from the actual heights, the content shifts, producing the "jump."

swift
1// This causes jumpy scrolling with dynamic cells
2tableView.estimatedRowHeight = 44  // Estimate is 44pt
3// But actual cells are 120pt, 80pt, 200pt — estimates are way off
4tableView.reloadData()  // Content jumps!

Solution 1: Cache Cell Heights (Best Fix)

Store the actual height of each cell after it is displayed, then return the cached value as the estimate:

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

This ensures that after the first display, estimated heights match actual heights exactly, eliminating jumps on reloadData().

Solution 2: Use Targeted Reloads Instead of reloadData()

Instead of reloading the entire table, update only the rows that changed:

swift
1// Instead of:
2// tableView.reloadData()
3
4// Use targeted updates:
5tableView.performBatchUpdates {
6    tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
7}
8
9// Or reload specific rows:
10tableView.reloadRows(at: [indexPath], with: .none)
11
12// Or reload a section:
13tableView.reloadSections(IndexSet(integer: 0), with: .none)

Targeted reloads only recalculate heights for the affected rows, preserving the scroll position for everything else.

Solution 3: Save and Restore Content Offset

Manually preserve the scroll position across a reload:

swift
1let contentOffset = tableView.contentOffset
2tableView.reloadData()
3tableView.layoutIfNeeded()
4tableView.setContentOffset(contentOffset, animated: false)

This is a simple workaround but does not address the root cause and may produce a brief visual flicker.

Solution 4: Use UITableViewDiffableDataSource (iOS 13+)

Diffable data sources automatically compute the minimal set of changes and animate them without jumps:

swift
1var dataSource: UITableViewDiffableDataSource<Section, Item>!
2
3func updateData(_ items: [Item]) {
4    var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
5    snapshot.appendSections([.main])
6    snapshot.appendItems(items, toSection: .main)
7
8    // Applies only the diff — no jumps
9    dataSource.apply(snapshot, animatingDifferences: true)
10}

This is the recommended approach for iOS 13+ as it eliminates the need for reloadData() in most cases.

Solution 5: Better Estimated Row Heights

If your cells have predictable height patterns, set a more accurate estimate:

swift
1// Bad: default estimate for all cells
2tableView.estimatedRowHeight = 44
3
4// Better: estimate based on your typical cell content
5tableView.estimatedRowHeight = 120  // Closer to average actual height
6
7// Best: implement the delegate method with per-cell estimates
8func tableView(_ tableView: UITableView,
9               estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
10    let item = items[indexPath.row]
11    if item.hasImage {
12        return 250
13    } else if item.text.count > 200 {
14        return 150
15    }
16    return 80
17}

Solution 6: Disable Estimated Heights Entirely

For small datasets, you can disable estimation entirely (not recommended for large lists):

swift
tableView.estimatedRowHeight = 0
tableView.estimatedSectionHeaderHeight = 0
tableView.estimatedSectionFooterHeight = 0

This forces the table view to calculate all cell heights upfront. It eliminates jumping but causes a performance hit on large datasets because all cells must be measured before display.

Complete Working Example

swift
1class FeedViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
2
3    @IBOutlet weak var tableView: UITableView!
4    var items: [FeedItem] = []
5    var heightCache: [IndexPath: CGFloat] = [:]
6
7    override func viewDidLoad() {
8        super.viewDidLoad()
9        tableView.delegate = self
10        tableView.dataSource = self
11        tableView.rowHeight = UITableView.automaticDimension
12        tableView.estimatedRowHeight = 120
13    }
14
15    // MARK: - Height Caching
16
17    func tableView(_ tableView: UITableView,
18                   willDisplay cell: UITableViewCell,
19                   forRowAt indexPath: IndexPath) {
20        heightCache[indexPath] = cell.frame.height
21    }
22
23    func tableView(_ tableView: UITableView,
24                   estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
25        return heightCache[indexPath] ?? 120
26    }
27
28    // MARK: - Data Updates
29
30    func refreshFeed(_ newItems: [FeedItem]) {
31        // Invalidate cache for changed items
32        for (index, item) in newItems.enumerated() {
33            if index < items.count && items[index].id != item.id {
34                heightCache.removeValue(forKey: IndexPath(row: index, section: 0))
35            }
36        }
37        items = newItems
38        tableView.reloadData()
39    }
40}

Common Pitfalls

  • Forgetting to invalidate the cache: When data changes (items reordered, deleted, or content updated), the cached heights become stale. Clear the cache for affected index paths before reloading.
  • Setting estimatedRowHeight to 0 on large datasets: This forces the table view to measure every cell upfront, causing a noticeable delay before the table appears. Only use this for tables with fewer than ~50 rows.
  • Auto Layout ambiguity: If your cell's constraints do not fully define the height, the table view falls back to an incorrect height. Verify constraints pin from top to bottom of the contentView.
  • Section headers/footers: The same jumping issue affects section headers and footers. Cache their heights with estimatedHeightForHeaderInSection as well.
  • reloadData on background thread: Calling reloadData() from a background thread causes undefined behavior including jumps. Always dispatch to the main queue.

Summary

ApproachEffortEffectiveness
Cache cell heightsMediumBest — eliminates jumps
Targeted reloadsLowGreat — avoids full reload
DiffableDataSourceMediumGreat — modern approach
Better estimatesLowGood — reduces jumps
Save/restore offsetLowWorkaround — may flicker
Disable estimationLowWorks but slow for large lists

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

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

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.