Swift
UITableView
iOS Development
Loading Detection
Mobile App Development

How to detect the end of loading of UITableView

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

Detecting when a UITableView has finished loading all its cells is needed for tasks like scrolling to a specific row, running animations, taking snapshots, or measuring layout. The challenge is that UITableView loads cells lazily and reloadData() returns immediately before the cells are rendered. There is no built-in delegate method for "loading complete," but several reliable techniques exist.

Method 1: layoutIfNeeded() After reloadData()

Force a synchronous layout after reloading:

swift
1tableView.reloadData()
2tableView.layoutIfNeeded()
3
4// Table is now fully laid out — safe to scroll or measure
5tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)

layoutIfNeeded() forces the table view to lay out all visible cells immediately. This is the simplest approach but blocks the main thread until layout completes.

Method 2: Dispatch to Next Run Loop

Use DispatchQueue.main.async to execute code after the current layout cycle:

swift
1tableView.reloadData()
2
3DispatchQueue.main.async {
4    // Executes after tableView has finished loading visible cells
5    print("Table view finished loading")
6    self.scrollToBottom()
7}

This works because reloadData() schedules layout on the current run loop, and DispatchQueue.main.async executes after that layout pass completes.

Method 3: CATransaction Completion Block

Wrap reloadData() in a CATransaction to get a completion callback:

swift
1CATransaction.begin()
2CATransaction.setCompletionBlock {
3    // Called after all animations and layout from reloadData() complete
4    print("Table view loading complete")
5    self.tableView.scrollToRow(
6        at: IndexPath(row: self.items.count - 1, section: 0),
7        at: .bottom,
8        animated: true
9    )
10}
11
12tableView.reloadData()
13
14CATransaction.commit()

This is the most reliable approach — CATransaction captures all implicit animations triggered by reloadData() and calls the completion block when they finish.

Method 4: willDisplay / didEndDisplaying

Track cell display events to detect when loading completes:

swift
1class ViewController: UIViewController, UITableViewDelegate {
2    private var cellsBeingDisplayed = 0
3
4    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell,
5                    forRowAt indexPath: IndexPath) {
6        cellsBeingDisplayed += 1
7    }
8
9    func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell,
10                    forRowAt indexPath: IndexPath) {
11        cellsBeingDisplayed -= 1
12    }
13}

A more practical approach — detect when the last row is displayed:

swift
1func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell,
2                forRowAt indexPath: IndexPath) {
3    let lastSection = tableView.numberOfSections - 1
4    let lastRow = tableView.numberOfRows(inSection: lastSection) - 1
5
6    if indexPath.section == lastSection && indexPath.row == lastRow {
7        print("Last cell is about to be displayed")
8        // Note: this triggers for the last VISIBLE cell, not necessarily the last data item
9    }
10}

Method 5: Custom ReloadData with Completion

Create a UITableView extension that provides a completion handler:

swift
1extension UITableView {
2    func reloadData(completion: @escaping () -> Void) {
3        CATransaction.begin()
4        CATransaction.setCompletionBlock(completion)
5        reloadData()
6        CATransaction.commit()
7    }
8}
9
10// Usage
11tableView.reloadData {
12    print("Loading complete!")
13    self.tableView.scrollToRow(
14        at: IndexPath(row: 0, section: 0),
15        at: .top,
16        animated: true
17    )
18}

This extension is the cleanest API for handling post-reload actions.

Method 6: performBatchUpdates Completion

For targeted updates (insert, delete, reload sections):

swift
1tableView.performBatchUpdates({
2    tableView.insertRows(at: newIndexPaths, with: .automatic)
3    tableView.deleteRows(at: removedIndexPaths, with: .automatic)
4}) { finished in
5    if finished {
6        print("Batch updates complete")
7        self.scrollToBottom()
8    }
9}

performBatchUpdates provides a built-in completion handler — no workarounds needed.

Method 7: KVO on contentSize

Observe the table view's content size changes:

swift
1class ViewController: UIViewController {
2    private var observation: NSKeyValueObservation?
3
4    override func viewDidLoad() {
5        super.viewDidLoad()
6
7        observation = tableView.observe(\.contentSize, options: [.new]) { [weak self] tableView, change in
8            guard let newSize = change.newValue else { return }
9            print("Content size changed: \(newSize)")
10            // Content size stabilizes after loading
11        }
12    }
13
14    deinit {
15        observation?.invalidate()
16    }
17}

This fires multiple times during loading. Debounce or check for stability if you need a single "done" signal.

Detecting End of Pagination Loading

For infinite scroll / load-more patterns:

swift
1func scrollViewDidScroll(_ scrollView: UIScrollView) {
2    let offsetY = scrollView.contentOffset.y
3    let contentHeight = scrollView.contentSize.height
4    let frameHeight = scrollView.frame.height
5
6    // Trigger load when user scrolls near the bottom
7    if offsetY > contentHeight - frameHeight - 100 {
8        loadMoreData()
9    }
10}
11
12func loadMoreData() {
13    guard !isLoading else { return }
14    isLoading = true
15
16    fetchNextPage { [weak self] newItems in
17        guard let self = self else { return }
18        self.items.append(contentsOf: newItems)
19
20        let indexPaths = (self.items.count - newItems.count..<self.items.count)
21            .map { IndexPath(row: $0, section: 0) }
22
23        self.tableView.performBatchUpdates({
24            self.tableView.insertRows(at: indexPaths, with: .automatic)
25        }) { _ in
26            self.isLoading = false
27        }
28    }
29}

Common Pitfalls

  • reloadData() is asynchronous: It returns immediately but cells are not rendered yet. Never access cell properties or scroll right after reloadData() without using one of the techniques above.
  • willDisplay fires for recycled cells: willDisplay is called every time a cell becomes visible, not just during initial load. Guard against duplicate logic if using this for one-time setup.
  • Multiple reloadData() calls: Rapid consecutive reloadData() calls may cause the completion block to fire for an intermediate state. Debounce or cancel previous operations.
  • Empty table views: If the data source returns 0 rows, willDisplay is never called. Handle the empty state separately.
  • Background thread: reloadData() and all UIKit calls must happen on the main thread. Calling from a background thread causes undefined behavior or crashes.

Summary

  • Use CATransaction with a completion block for the most reliable post-reload callback
  • Use the UITableView extension reloadData(completion:) for a clean API
  • Use DispatchQueue.main.async after reloadData() for a simple one-liner
  • Use performBatchUpdates for insert/delete operations — it has a built-in completion handler
  • Use willDisplay delegate to detect when the last visible cell is rendered
  • Never access cells or scroll immediately after reloadData() — cells are not rendered yet

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.