UITableView Infinite Scrolling Tutorial

In this tutorial you will learn how to implement an Infinite Scrolling UITableView in your iOS app using a paginated REST API. By Lorenzo Boaro.

4.2 (29) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Infinite Scrolling: Requesting Next Pages

You need to modify the view model code to request the next pages of the API. Here’s an overview of what you need to do:

  • Keep track of the last page received so you know which page is needed next when the UI calls the request method
  • Build the full list of moderators. When you receive a new page from the API, you have to add it to your moderator’s list (instead of replacing it like you were doing before). When you get a response, you can update the table view to include all of the moderators received thus far.

Open ModeratorsViewModel.swift, and add the following method below fetchModerators() :

private func calculateIndexPathsToReload(from newModerators: [Moderator]) -> [IndexPath] {
  let startIndex = moderators.count - newModerators.count
  let endIndex = startIndex + newModerators.count
  return (startIndex..<endIndex).map { IndexPath(row: $0, section: 0) }
}

This utility calculates the index paths for the last page of moderators received from the API. You'll use this to refresh only the content that's changed, instead of reloading the whole table view.

Now, head to fetchModerators(). Find the success case and replace its entire content with the following:

DispatchQueue.main.async {
  // 1
  self.currentPage += 1
  self.isFetchInProgress = false
  // 2
  self.total = response.total
  self.moderators.append(contentsOf: response.moderators)
  
  // 3
  if response.page > 1 {
    let indexPathsToReload = self.calculateIndexPathsToReload(from: response.moderators)
    self.delegate?.onFetchCompleted(with: indexPathsToReload)
  } else {
    self.delegate?.onFetchCompleted(with: .none)
  }
}

There’s quite a bit going on here, so let’s break it down:

  1. If the response is successful, increment the page number to retrieve. Remember that the API request pagination is defaulted to 30 items. Fetch the first page, and you'll retrieve the first 30 items. With the second request, you'll retrieve the next 30, and so on. The retrieval mechanism will continue until you receive the full list of moderators.
  2. Store the total count of moderators available on the server. You'll use this information later to determine whether you need to request new pages. Also store the newly returned moderators.
  3. If this isn't the first page, you'll need to determine how to update the table view content by calculating the index paths to reload.

You can now request all of the pages from the total list of moderators, and you can aggregate all of the information. However, you still need to request the appropriate pages dynamically when scrolling.

Building the Infinite User Interface

To get the infinite scrolling working in your user interface, you first need to tell the table view that the number of cells in the table is the total number of moderators, not the number of moderators you have loaded. This allows the user to scroll past the first page, even though you still haven't received any of those moderators. Then, when the user scrolls past the last moderator, you need to request a new page.

You'll use the Prefetching API to determine when to load new pages. Before starting, take a moment to understand how this new API works.

UITableView defines a protocol, named UITableViewDataSourcePrefetching, with the following two methods:

  • tableView(_:prefetchRowsAt:): This method receives index paths for cells to prefetch based on current scroll direction and speed. Usually you'll write code to kick off data operations for the items in question here.
  • tableView(_:cancelPrefetchingForRowsAt:): An optional method that triggers when you should cancel prefetch operations. It receives an array of index paths for items that the table view once anticipated but no longer needs. This might happen if the user changes scroll directions.

Since the second one is optional, and you're interested in retrieving new content only, you'll use just the first method.

Note: If you're using a collection view instead of a table view, you can get similar behaviour by implementing UICollectionViewDataSourcePrefetching.

Note: If you're using a collection view instead of a table view, you can get similar behaviour by implementing UICollectionViewDataSourcePrefetching.

In the Controllers group, open ModeratorsListViewController.swift, and have a quick look. This controller implements the data source for UITableView and calls fetchModerators() in viewDidLoad() to load the first page of moderators. But it doesn't do anything when the user scrolls down the list. Here's where the Prefetching API comes to the rescue.

First, you have to tell the table view that you want to use Prefetching. Find viewDidLoad() and insert the following line just below the line where you set the data source for the table view:

tableView.prefetchDataSource = self

This causes the compiler to complain because the controller doesn't yet implement the required method. Add the following extension at the end of the file:

extension ModeratorsListViewController: UITableViewDataSourcePrefetching {
  func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
    
  }
}

You'll implement its logic soon, but before doing so, you need two utility methods. Move to the end of the file, and add a new extension:

private extension ModeratorsListViewController {
  func isLoadingCell(for indexPath: IndexPath) -> Bool {
    return indexPath.row >= viewModel.currentCount
  }

  func visibleIndexPathsToReload(intersecting indexPaths: [IndexPath]) -> [IndexPath] {
    let indexPathsForVisibleRows = tableView.indexPathsForVisibleRows ?? []
    let indexPathsIntersection = Set(indexPathsForVisibleRows).intersection(indexPaths)
    return Array(indexPathsIntersection)
  }
}
  • isLoadingCell(for:): Allows you to determine whether the cell at that index path is beyond the count of the moderators you have received so far.
  • visibleIndexPathsToReload(intersecting:): This method calculates the cells of the table view that you need to reload when you receive a new page. It calculates the intersection of the IndexPaths passed in (previously calculated by the view model) with the visible ones. You'll use this to avoid refreshing cells that are not currently visible on the screen.

With these two methods in place, you can change the implementation of tableView(_:prefetchRowsAt:). Replace it with this:

func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
  if indexPaths.contains(where: isLoadingCell) {
    viewModel.fetchModerators()      
  }
}

As soon as the table view starts to prefetch a list of index paths, it checks if any of those are not loaded yet in the moderators list. If so, it means you have to ask the view model to request a new page of moderatos. Since tableView(_:prefetchRowsAt:) can be called multiple times, the view model — thanks to its isFetchInProgress property — knows how to deal with it and ignores subsequent requests until it's finished.

Now it is time to make a few changes to the UITableViewDataSource protocol implementation. Find the associated extension and replace it with the following:

extension ModeratorsListViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // 1
    return viewModel.totalCount
  }
  
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.list, 
               for: indexPath) as! ModeratorTableViewCell
    // 2
    if isLoadingCell(for: indexPath) {
      cell.configure(with: .none)
    } else {
      cell.configure(with: viewModel.moderator(at: indexPath.row))
    }
    return cell
  }
}

Here's what you've changed:

  1. Instead of returning the count of the moderators you've received already, you return the total count of moderators available on the server so that the table view can show a row for all the expected moderators, even if the list is not complete yet.
  2. If you haven't received the moderator for the current cell, you configure the cell with an empty value. In this case, the cell will show a spinning indicator view. If the moderator is already on the list, you pass it to the cell, which shows the name and reputation.

You're almost there! You need to refresh the user interface when you receive data from the API. In this case, you need to act differently depending on the page received.

When you receive the first page, you have to hide the main waiting indicator, show the table view and reload its content.
But when you receive the next pages, you need to reload the cells that are currently on screen (using the visibleIndexPathsToReload(intersecting:) method you added earlier.

Still in ModeratorsListViewController.swift, find onFetchCompleted(with:) and replace it with this:

func onFetchCompleted(with newIndexPathsToReload: [IndexPath]?) {
  // 1
  guard let newIndexPathsToReload = newIndexPathsToReload else {
    indicatorView.stopAnimating()
    tableView.isHidden = false
    tableView.reloadData()
    return
  }
  // 2
  let indexPathsToReload = visibleIndexPathsToReload(intersecting: newIndexPathsToReload)
  tableView.reloadRows(at: indexPathsToReload, with: .automatic)
}

Here's the breakdown:

  1. If newIndexPathsToReload is nil (first page), hide the indicator view, make the table view visible and reload it.
  2. If newIndexPathsToReload is not nil (next pages), find the visible cells that needs reloading and tell the table view to reload only those.