Infinite Scrolling


Getting Started

First, download the starter project and open ‘ViewController.swift’.
At the top of this class, I’ve declared a tableView and an array of items.

1
2
var tableView:UITableView!
var items = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]

Next, in the viewDidLoad function a few things happen:

  1. Instantiate the tableView
    1
    tableView = UITableView(frame: view.bounds, style: .plain)
  2. Register the tableView
    1
    2
    3
    4
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "tableCell")
    let loadingNib = UINib(nibName: "LoadingCell", bundle: nil)
    tableView.register(loadingNib, forCellReuseIdentifier: "loadingCell")
    tableView.delegate = self

LoadingCell is an empty tableView cell with a centered UIActivityIndicator. Copy it from the project or build your own!

  1. Assign the tableView delegate & datasource
    1
    2
    tableView.delegate = self
    tableView.dataSource = self
  2. Add and constrain the tableView to the main view
    1
    2
    3
    4
    5
    6
    7
    view.addSubview(tableView)
    tableView.translatesAutoresizingMaskIntoConstraints = false
    let layoutGuide = view.safeAreaLayoutGuide
    tableView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor).isActive = true
    tableView.topAnchor.constraint(equalTo: layoutGuide.topAnchor).isActive = true
    tableView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor).isActive = true
    tableView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor).isActive = true
  3. Reload the tableView
    1
    tableView.reloadData()

    Implement Delegate & Datasource methods

Next, add the following methods below viewDidLoad to tell the tableView to display a cell for each item in in items.

1
2
3
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
1
2
3
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
1
2
3
4
5
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "tableCell", for: indexPath)
cell.textLabel?.text = "Item \(items[indexPath.row])"
return cell
}

Detecting ScrollView Bottom

Now, we need a way to detect when the user scrolls to the bottom of the tableView.
Fortunately, we have already implemented UITableViewDelegate which is a superclass of UIScrollViewDelegate,
meaning we can also implement to UIScrollViewDelegate methods.
We want to observe anytime the user scrolls the tableView so let’s implement scrollViewDidScroll:

1
func scrollViewDidScroll(_ scrollView:UIScrollView) {}

Next, assign a variable to the scrollView.contentOffset.y and another to the scrollView.contentSize.height.

1
2
3
4
func scrollViewDidScroll(_ scrollView:UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
}

The offsetY variable is the amount the user has scrolled from the top of the tableView (0,0).
UITableView‘s are inherently vertical scrollViews, so we are only interested in the y axis;
however, in general, UIScrollViews may scroll on any axis.
A positive contentOffset indicates that user is scrolling down (revealing the bottom),
while a negative contentOffset indicates that the user is scrolling up (revealing the top).

The contentHeight variable is the height of the content within the scrollView.
It is important to distinguish that the content height is different from the height of the scrollView itself,
and is usually greater or less than the height of the scrollView (hence, why we need to scroll!).

Next, in order to detect if the user has reached the bottom of the scrollView add the following block:

1
2
3
4
5
6
...
if offsetY > contentHeight - scrollView.frame.height {
if !fetchingMore {
beginBatchFetched()
}
}

Add a new Bool variable at the top of the class:

1
var fetchingMore = false

This flag will indicate whether we are currently fetching more data or not and will prevent us from adding duplicate data to our tableView.
Next, add a new method called beginBatchFetched:

1
2
3
func beginBatchFetched() {
fetchingMore = true
}

Inside this method, we can fetch whatever data we would like to add to our tableView.
In this example we don’t have any API to call so we will fake it with a DispatchQueue that waits for 0.5 seconds,
adds 12 new items to the items array, then reloads the tableView.
Also notice that we set fetchingMore back to false at the end!

1
2
3
4
5
6
7
8
9
...
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
let newItems = (self.items.count...self.items.count + 12).map { index in
return index
}
self.items.append(contentsOf: newItems)
self.fetchingMore = false
self.tableView.reloadData()
})

And that’s it! Test it out, we now have an infinite scrolling tableView!

However there are a few things we can improve before we finish…