
This video is only available to subscribers. Start a subscription today to get access to this and 472 other videos.
UITableView Prefetching
Episode Links
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!