Swift
tableView
Pagination
iOS Development
Mobile App Development

Swift tableView Pagination

Master System Design with Codemia

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

Introduction

Pagination in UITableView keeps scrolling smooth by loading data incrementally instead of downloading everything at once. A robust implementation needs more than a scroll trigger: you also need loading state control, duplicate request prevention, error handling, and end-of-list detection. With a clean architecture, pagination remains stable under fast scrolling and flaky networks.

Choose a Pagination Style

Most APIs use one of two styles:

  • page-based, using page number and page size
  • cursor-based, using a token returned by previous response

Cursor-based pagination is generally safer for changing datasets because it avoids missing or duplicated records when new items are inserted on the server.

Define Model and Response Types

swift
1struct Item: Decodable {
2    let id: String
3    let title: String
4}
5
6struct PageResponse: Decodable {
7    let items: [Item]
8    let nextCursor: String?
9}

Keeping the response structure explicit simplifies end-of-list logic.

Manage Pagination State in View Controller

swift
1final class ItemsViewController: UIViewController {
2    @IBOutlet private weak var tableView: UITableView!
3
4    private var items: [Item] = []
5    private var nextCursor: String?
6    private var isLoading = false
7    private var reachedEnd = false
8
9    override func viewDidLoad() {
10        super.viewDidLoad()
11        tableView.dataSource = self
12        tableView.delegate = self
13        loadNextPageIfNeeded()
14    }
15}

These flags prevent overlapping requests and repeated end-of-list fetches.

Fetch Next Page Safely

swift
1extension ItemsViewController {
2    private func loadNextPageIfNeeded() {
3        guard !isLoading, !reachedEnd else { return }
4        isLoading = true
5
6        API.fetchItems(cursor: nextCursor) { [weak self] result in
7            guard let self else { return }
8            self.isLoading = false
9
10            switch result {
11            case .success(let response):
12                self.items.append(contentsOf: response.items)
13                self.nextCursor = response.nextCursor
14                self.reachedEnd = response.nextCursor == nil || response.items.isEmpty
15                self.tableView.reloadData()
16
17            case .failure(let error):
18                self.showError(error)
19            }
20        }
21    }
22
23    private func showError(_ error: Error) {
24        print("pagination error: \(error.localizedDescription)")
25    }
26}

A single entry point for pagination requests keeps behavior predictable.

Trigger Loading Near the Bottom

Use willDisplay to fetch before user reaches end.

swift
1extension ItemsViewController: UITableViewDelegate {
2    func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
3        let threshold = max(items.count - 5, 0)
4        if indexPath.row >= threshold {
5            loadNextPageIfNeeded()
6        }
7    }
8}

The threshold gives a prefetch effect and reduces visible loading stalls.

A footer spinner gives user feedback during requests.

swift
1private func setLoadingFooter(_ loading: Bool) {
2    guard loading else {
3        tableView.tableFooterView = UIView(frame: .zero)
4        return
5    }
6
7    let spinner = UIActivityIndicatorView(style: .medium)
8    spinner.startAnimating()
9    spinner.frame = CGRect(x: 0, y: 0, width: tableView.bounds.width, height: 44)
10    tableView.tableFooterView = spinner
11}

Call this when isLoading changes.

Avoid Duplicate and Out-of-Order Data

When users scroll fast, you may get delayed responses arriving out of order. Protect against stale responses by tracking request tokens or cancelling old tasks. If using URLSession with async code, cancel prior request before starting new one when logic requires strict ordering.

Also deduplicate by id if backend can return overlapping windows.

Support Pull-to-Refresh with Pagination Reset

Refreshing should reset pagination state and request first page again.

swift
1private func refreshAll() {
2    nextCursor = nil
3    reachedEnd = false
4    items.removeAll()
5    tableView.reloadData()
6    loadNextPageIfNeeded()
7}

Without reset logic, pull-to-refresh can produce inconsistent lists.

Common Pitfalls

  • Triggering multiple concurrent requests due to missing isLoading guard.
  • Not handling end-of-list and requesting forever.
  • Using exact last-cell trigger, causing visible loading pauses.
  • Ignoring duplicate records when backend windows overlap.
  • Forgetting to reset pagination state during full refresh.

Summary

  • 'UITableView pagination needs state management, not only a scroll callback.'
  • Use clear flags for loading state and end-of-list behavior.
  • Trigger next-page fetch slightly before the final rows.
  • Handle errors, deduplication, and refresh resets explicitly.
  • Cursor-based APIs generally provide more reliable pagination under changing data.

Course illustration
Course illustration

All Rights Reserved.