UITableView Infinite Scrolling Tutorial

Note: This tutorial works with both Xcode 9 and Xcode 10, iOS 11 and iOS 12.

Infinite scrolling allows users to load content continuously, eliminating the need for pagination. The app loads some initial data and then adds the rest of the information when the user reaches the bottom of the visible content.

Social media companies like Twitter and Facebook have made this technique popular over the years. If you look at their mobile applications, you can see infinite scrolling in action.

In this tutorial, you’ll learn how to add infinite scrolling to an iOS app that fetches data from a REST API. In particular, you’ll integrate the Stack Exchange REST API to display the list of moderators for a specific site, like Stack Overflow or Mathematics.

To improve the app experience, you’ll use the Prefetching API introduced by Apple in iOS 10 for both UITableView and UICollectionView. This is an adaptive technology that performs optimizations targeted to improve scrolling performances. Data source prefetching provides a mechanism to prepare data before you need to display it. For large data sources where fetching the information takes time, implementing this technology can have a dramatic impact on user experience.

Getting Started

For this tutorial, you’ll use ModeratorsExplorer, an iOS app that uses the Stack Exchange REST API to display the moderators for a specific site.

Start by downloading the starter project using the Download Materials link at the top or bottom of this tutorial. Once downloaded, open ModeratorsExplorer.xcodeproj in Xcode.

To keep you focused, the starter project has everything unrelated to infinite scrolling already set up for you.

In Views, open Main.storyboard and look at the view controllers contained within:

storyboard

The view controller on the left is the root navigation controller of the app. Then you have:

  1. ModeratorsSearchViewController: This contains a text field so you can search for a site. It also contains a button which takes you to the next view.
  2. ModeratorsListViewController: This includes a table which lists the moderators for a given site. Each table cell, of type ModeratorTableViewCell, includes two labels: one to display the name of the moderator and one for the reputation. There’s also a busy indicator that spins when new content is requested.

Build and run the app, and you’ll see the initial screen:

startscreen

At the moment, tapping on Find Moderators! will show a spinner that animates indefinitely. Later in this tutorial, you’ll hide that spinner once the initial content gets loaded.

Get Acquainted with Stack Exchange API

The Stack Exchange API provides a mechanism to query items from the Stack Exchange network.

For this tutorial, you’re going to use the /users/moderators API. As the name implies, it returns the list of moderators for a specific site.

The API response is paginated; the first time you request the list of moderators, you won’t receive the whole list. Instead, you’ll get a list with a limited number of the moderators (a page) and a number indicating the total number of moderators in their system.

Pagination is a common technique for many public APIs. Instead of sending you all the data they have, they send a limited amount; when you need more, you make another request. This saves server resources and provides a faster response.

Here’s the JSON response (for clarity, it only shows the fields related to pagination):

{

  "has_more": true,
  "page": 1,
  "total": 84,
  "items": [
 
    ...
    ...
  ]
}

The response includes the total number of moderators in their system (84) and the requested page (1). With this information, and the list of moderators received, you can determine the number of items and pages you need to request to show the complete list.

If you want to learn more about this specific API, visit Usage of /users/moderators.

Show Me The Moderators

Note: This tutorial uses URLSession to implement the network client. If you’re not familiar with it, you can learn about it in URLSession Tutorial: Getting Started or in our course Networking with URLSession.

Start by loading the first page of moderators from the API.

In Networking, open StackExchangeClient.swift and find fetchModerators(with:page:completion:). Replace the method with this:

func fetchModerators(with request: ModeratorRequest, page: Int, 
     completion: @escaping (Result<PagedModeratorResponse, DataResponseError>) -> Void) {
  // 1
  let urlRequest = URLRequest(url: baseURL.appendingPathComponent(request.path))
  // 2
  let parameters = ["page": "\(page)"].merging(request.parameters, uniquingKeysWith: +)
  // 3
  let encodedURLRequest = urlRequest.encode(with: parameters)
  
  session.dataTask(with: encodedURLRequest, completionHandler: { data, response, error in
    // 4
    guard 
      let httpResponse = response as? HTTPURLResponse,
      httpResponse.hasSuccessStatusCode,
      let data = data 
    else {
        completion(Result.failure(DataResponseError.network))
        return
    }
    
    // 5
    guard let decodedResponse = try? JSONDecoder().decode(PagedModeratorResponse.self, from: data) else {
      completion(Result.failure(DataResponseError.decoding))
      return
    }
    
    // 6
    completion(Result.success(decodedResponse))
  }).resume()
}

Here’s the breakdown:

  1. Build a request using URLRequest initializer. Prepend the base URL to the path required to get the moderators. After its resolution, the path will look like this:
    http://api.stackexchange.com/2.2/users/moderators.
  2. Create a query parameter for the desired page number and merge it with the default parameters defined in the ModeratorRequest instance — except for the page and the site; the former is calculated automatically each time you perform a request, and the latter is read from the UITextField in ModeratorsSearchViewController.
  3. Encode the URL with the parameters created in the previous step. Once done, the final URL for a request should look like this: http://api.stackexchange.com/2.2/users/moderators?site=stackoverflow&page=1&filter=!-*jbN0CeyJHb&sort=reputation&order=desc. Create a URLSessionDataTask with that request.
  4. Validate the response returned by the URLSession data task. If it’s not valid, invoke the completion handler and return a network error result.
  5. If the response is valid, decode the JSON into a PagedModeratorResponse object using the Swift Codable API. If it finds any errors, call the completion handler with a decoding error result.
  6. Finally, if everything is OK, call the completion handler to inform the UI that new content is available.

Now it’s time to work on the moderators list. In ViewModels, open ModeratorsViewModel.swift, and replace the existing definition of fetchModerators with this one:

func fetchModerators() {
  // 1
  guard !isFetchInProgress else {
    return
  }
  
  // 2
  isFetchInProgress = true
  
  client.fetchModerators(with: request, page: currentPage) { result in
    switch result {
    // 3
    case .failure(let error):
      DispatchQueue.main.async {
        self.isFetchInProgress = false
        self.delegate?.onFetchFailed(with: error.reason)
      }
    // 4
    case .success(let response):
      DispatchQueue.main.async {
        self.isFetchInProgress = false
        self.moderators.append(contentsOf: response.moderators)          
        self.delegate?.onFetchCompleted(with: .none)
      }
    }
  }
}

Here’s what’s happening with the code you just added:

  1. Bail out, if a fetch request is already in progress. This prevents multiple requests happening. More on that later.
  2. If a fetch request is not in progress, set isFetchInProgress to true and send the request.
  3. If the request fails, inform the delegate of the reason for that failure and show the user a specific alert.
  4. If it’s successful, append the new items to the moderators list and inform the delegate that there’s data available.

Note: In both the success and failure cases, you need to tell the delegate to perform its work on the main thread: DispatchQueue.main. This is necessary since the request happens on a background thread and you’re going to manipulate UI elements.

Build and run the app. Type stackoverflow in the text field and tap on Find Moderators. You’ll see a list like this:

Moderators list

Hang on! Where’s the rest of the data? If you scroll to the end of the table, you’ll notice it’s not there.

By default, the API request returns only 30 items for each page, therefore, the app shows the first page with the first 30 items. But, how do you present all of the moderators?

You need to modify the app so it can request the rest of the moderators. When you receive them, you need to add those new items to the list. You incrementally build the full list with each request, and you show them in the table view as soon as they’re ready.

You also need to modify the user interface so it can react when the user scrolls down the list. When they get near the end of the list of loaded moderators, you need to request a new page.

Because network requests can take a long time, you need to improve the user experience by displaying a spinning indicator view if the moderator information is not yet available.

Time to get to work!

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.

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.

Infinite Scrolling in Action

It's time to see the result of all your hard work! :]

Build and run the app. When the app launches, you'll see the search view controller.

Type stackoverflow into the text field and tap the Find Moderators! button. When the first request completes and the waiting indicator disappears, you'll see the initial content. If you start scrolling to the bottom, you may notice a few cells showing a loading indicator for the moderators that haven't been received yet.

Cells loading

When a request completes, the app hides the spinners and shows the moderator information in the cell. The infinite loading mechanism continues until no more items are available.

Infinite Scrolling

Note: If the network activities occur too quickly to see your cells spinning and you're running on an actual device, you can make sure this works by toggling some network settings in the Developer section of the Settings app. Go to the Network Link Conditioner section, enable it, and select a profile. Very Bad Network is a good choice.

If you're running on the Simulator, you can use the Network Link Conditioner included in the Advanced Tools for Xcode to change your network speed. This is a good tool to have in your arsenal because it forces you to be conscious of what happens to your apps when connection speeds are less than optimal.

Hurray! This is the end of your hard work. :]

Where to Go From Here?

You can download the completed version of the project using the Download Materials link at the top or the bottom of this tutorial.

You’ve learned how to achieve infinite scrolling and take advantage of the iOS Prefetching API. Your users can now scroll through a potentially unlimited number of cells. You also learned how to deal with a paginated REST API like the Stack Exchange API.

If you want to learn more about iOS' prefetching API, check out Apple's documentation at What's New in UICollectionView in iOS 10, our book iOS 10 by Tutorials or Sam Davies's free screencast on iOS 10: Collection View Data Prefetching.

In the meantime, if you have any questions or comments, please join the forum discussion below!

Download Materials

Team

Each tutorial at www.raywenderlich.com is created by a team of dedicated developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Lorenzo Boaro

Lorenzo is a Software Engineer who is passionate about mobile development. He has been engineering iOS apps since 2010 but he is comfortable with other languages such as C#, JavaScript and Java.

He is not only fond of iOS development but he also has different research interests. In fact, during his Ph.D., he focused his effort on Web Services, Specification and Verification of Software Systems, Business Process Management, Case Management, Intelligent User Interfaces and Healthcare Systems.

In his spare time he is studying to become an actor.

Other Items of Interest

Save time.
Learn more with our video courses.

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 27 total!

iOS Team

... 83 total!

Android Team

... 47 total!

Unity Team

... 16 total!

Articles Team

... 4 total!

Resident Authors Team

... 32 total!

Podcast Team

... 4 total!

Recruitment Team

... 8 total!

Illustration Team

... 4 total!