UITableView
iOS Development
Scrolling Detection
Swift
Mobile App Development

Detect when UITableView has scrolled to the bottom

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Detecting when a UITableView reaches the bottom is a common requirement for infinite scrolling, lazy loading, and analytics. The practical solution is to compare the current scroll position with the total content height, while also guarding against repeated triggers when the user lingers near the bottom.

Using scrollViewDidScroll

Because UITableView is a UIScrollView, you can detect bottom reach in the scroll delegate.

swift
1import UIKit
2
3class ItemsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
4    @IBOutlet weak var tableView: UITableView!
5
6    private var items = Array(0..<50)
7    private var isLoadingMore = false
8
9    override func viewDidLoad() {
10        super.viewDidLoad()
11        tableView.delegate = self
12        tableView.dataSource = self
13    }
14
15    func scrollViewDidScroll(_ scrollView: UIScrollView) {
16        let offsetY = scrollView.contentOffset.y
17        let contentHeight = scrollView.contentSize.height
18        let visibleHeight = scrollView.bounds.height
19        let bottomInset = scrollView.adjustedContentInset.bottom
20
21        let threshold = contentHeight - visibleHeight - bottomInset - 100
22
23        if offsetY > threshold && !isLoadingMore {
24            loadMore()
25        }
26    }
27
28    private func loadMore() {
29        isLoadingMore = true
30        print("Load more data")
31
32        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
33            let next = self.items.count..<(self.items.count + 20)
34            self.items.append(contentsOf: next)
35            self.tableView.reloadData()
36            self.isLoadingMore = false
37        }
38    }
39
40    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
41        items.count
42    }
43
44    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
45        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
46        cell.textLabel?.text = "Row \(items[indexPath.row])"
47        return cell
48    }
49}

The threshold lets you start loading slightly before the exact bottom, which usually feels better than waiting for the user to hit the final pixel.

Why the Load Guard Matters

Without a flag such as isLoadingMore, bottom detection fires repeatedly while the scroll view remains near the threshold. That can cause:

  • duplicate network requests
  • duplicate rows
  • wasted work

The guard turns the bottom detection into a one-shot trigger until the current load finishes.

Alternative: willDisplay the Last Cell

For some table views, detecting when the last cell is about to appear is even simpler:

swift
1func tableView(_ tableView: UITableView,
2               willDisplay cell: UITableViewCell,
3               forRowAt indexPath: IndexPath) {
4    let lastRow = items.count - 1
5    if indexPath.row == lastRow && !isLoadingMore {
6        loadMore()
7    }
8}

This works well for straightforward infinite scrolling, though it is less precise if your content size changes dramatically or if you need prefetching before the final row becomes visible.

Insets and Empty Tables

When you compute the bottom position, include content insets or safe-area adjustments. Using adjustedContentInset is usually better than ignoring insets entirely.

Also consider empty or very short tables. If the content is smaller than the visible height, your threshold logic may trigger immediately on first layout. Sometimes that is desired, but sometimes you want to block load-more behavior until initial data is already present.

Common Pitfalls

The most common mistake is forgetting to prevent repeated triggers. If loadMore() can fire multiple times before the previous request finishes, infinite scrolling quickly becomes unstable.

Another issue is calculating the threshold from frame.height without accounting for insets. Safe areas and content insets can make the "bottom" appear earlier or later than expected.

A third pitfall is calling reloadData() too aggressively for large tables. For append-heavy lists, inserting rows or using diffable updates can be smoother than full reloads.

Finally, do not treat "scrolled near bottom" and "finished loading more data" as the same state. Keep explicit state for loading so the table does not duplicate work.

Summary

  • Use scrollViewDidScroll to compare offset, content height, and visible height.
  • Add a threshold so loading can begin before the user reaches the exact bottom.
  • Guard with an isLoadingMore flag to prevent duplicate requests.
  • 'willDisplay on the last cell is a valid simpler alternative in some cases.'
  • Account for insets, empty content, and repeated callback behavior.

Course illustration
Course illustration

All Rights Reserved.