Extending our example from episode 309, here we implement automatic tableview paging support by utilizing the UITableViewDatasourcePrefetching protocol. With this protocol, our delegate is notified of upcoming rows the user is about to encounter, and gives us an opportunity to preemptively load data for those rows.
Episode Links UITableViewDataSourcePrefetching Setting up the prefetch datasource The first thing we need to do is adopt the protocol and set ourselves as the prefetchDataSource on our controller. extension BeerListViewController : UITableViewDataSourcePrefetching { func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { } func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { } } Once we’ve added this, we can adopt the protocol in viewDidLoad: override func viewDidLoad() { super.viewDidLoad() title = "Beers" tableView.prefetchDataSource = self //... } Change the DataSource return all the rows Our current datasource implementation only returns rows for beers we have already loaded in our beers array. We instead want to use a real value so that the user can scroll to data we may not have fetched yet. To do this, we’ll add a new property to our view controller: var totalBeerCount: Int = 0 We’ll update this value when we get a response from the API: private func loadBeers(refresh: Bool = false) { breweryDBClient.fetchBeers(page: currentPage, styleId: 3) { page in DispatchQueue.main.async { self.totalBeerCount = page.totalResults //... } And we can use this value in our numberOfRowsInSection: method: override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return totalBeerCount } Fixing the loading row Currently our loading row only shows if the shouldShowLoadingRow boolean is true. We don’t need this anymore, so we can remove it and all references to it. When the user scrolls beyond our loaded array they will see a bunch of loading rows. We also need to avoid trying to fetch the next page on loading row appearance, since this will now happen repeatedly: // REMOVE THIS METHOD override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard isLoadingIndexPath(indexPath) else { return } fetchNextPage() } Fetching Pages Now we can leverage the prefetching calls to fetch the next page when we get a request for an element that is not in the bounds of our array. func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let needsFetch = indexPaths.contains { $0.row >= self.beers.count } if needsFetch { fetchNextPage() } } This will happen repeatedly, so to avoid making multiple requests for the same page, we'll add a new property on our controller. private var isFetchingNextPage = false Then we can avoid fetching when this value is set: private func fetchNextPage() { guard !isFetchingNextPage else { return } currentPage += 1 loadBeers() } All we need to do now is update this value before a request is made and return it back to false after the response comes back. private func loadBeers(refresh: Bool = false) { print("Fetching page \(currentPage)") isFetchingNextPage = true breweryDBClient.fetchBeers(page: currentPage, styleId: 3) { page in // ... self.isFetchingNextPage = false } } Better Handling of New Rows Instead of blindly calling reloadData, we'll instead animate rows that have been updated and are currently visible. Using a Set is perfect for this. We'll also move the refreshControl.endRefreshing() call into the case where we forced a refresh, since this has the unfortunate side-effect of halting scrolling. breweryDBClient.fetchBeers(page: currentPage, styleId: 3) { page in DispatchQueue.main.async { self.totalBeerCount = page.totalResults if refresh { self.beers = page.data } else { for beer in page.data { if !self.beers.contains(beer) { self.beers.append(beer) } } } self.isFetchingNextPage = false if refresh { self.refreshControl?.endRefreshing() self.tableView.reloadData() } else { let startIndex = self.beers.count - page.data.count let endIndex = startIndex + page.data.count - 1 let newIndexPaths = (startIndex...endIndex).map { i in return IndexPath(row: i, section: 0) } let visibleIndexPaths = Set(self.tableView.indexPathsForVisibleRows ?? []) let indexPathsNeedingReload = Set(newIndexPaths).intersection(visibleIndexPaths) self.tableView.reloadRows(at: Array(indexPathsNeedingReload), with: .fade) } } } Now scrolling is super smooth and if the user scrolls slowly they’ll never see a loading row!