Infinite scrolling is another one of those clever UI controls that allows a user to scroll through a (potentially) unlimited number of table view cells. The visual effect is intended to lead your user to believe that there are no “pages” of content but instead a continuous list of content available to them. To create this illusion, the general technical implementation preloads some content into a list and then appends to that list as the user approaches the end of the loaded content.
With a Spinner
When dealing with content that lives over the network, true infinite scrolling is hard to achieve. Because we can’t always count on a fast and reliable network connection, we cannot always guarantee that the next chunk of available content will have downloaded in time to display it when the user reaches the bottom of the currently visible content. To solve this problem, spinners are commonly used to let the user know that they’ve reached the (pseudo) end of the visible content and need to wait while more of the available content is being downloaded. Once the content is downloaded, the spinner disappears and the additional content is appended to the visible list.
To achieve this effect, we can simply add a spinner to the UITableView footer and some logic that automatically fetches the next page of available information when we are approaching the bottom of the list.
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)
spinner.startAnimating()
spinner.frame = CGRect(x: 0, y: 0, width: self.tableView.frame.width, height: 44)
self.tableView.tableFooterView = spinner;
// ...
func tableView(_ tableView: UITableView,
willDisplay cell: UITableViewCell,
forRowAt indexPath: IndexPath)
{
// At the bottom...
if (indexPath.row == self.data.count - 1) {
getMoreData() // network request to get more data
}
}
Although described as “infinite scrolling”, this UI implementation is not
quite perfect as it can be apparent to the user that all of the content is
not visible and that they may need to wait while more of the available
content is being downloaded. It is possible to adjust the buffer
constant
to fetch more available content sooner, but unfortunately that will not
eliminate the possibility that the user will be waiting at the end of the
UITableView to get more content on a poor network connection.
Without a Spinner
But let’s say our content doesn’t have to be retrieved over the network and we already have all of the available content. Furthermore, what if we were displaying information (like dates) where there literally is no end to the list of possible content. In situations like these, infinite scrolling can be achieved without using spinners, leaving the user to believe that the visible content is one long continuous list. Even better, we can also guarantee that the user’s scroll will never be interrupted by reaching a “pseudo end”.
In the following example, I’d like to show you how we can achieve this effect
by using dates as our list content. Essentially we will be mimicking the
behavior of the native UIDatePicker
but instead our content will be displayed
in a UITableView
.
Setup
For brevity, I’ve created a starting point for this example that can be viewed
here. What we have here is a UITableView
which is being
populated with 60 date cells (29 in the past and 30 into the future). I’ve also
created a helper function to generate an array of days between two specified
dates, and an extension
on the NSDate
class to allow me to easily get a
date that is some number of days in the past or future.
Infinite scroll down
Infinite scrolling in the down direction is actually quite easy to implement.
All we need to do is check if we’ve reached the bottom of the list and append
more dates to our days
array.
let daysToAdd = 30
let cellBuffer: CGFloat = 2
// ...
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let top: CGFloat = 0
let bottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height
let buffer: CGFloat = self.cellBuffer * self.cellHeight
let scrollPosition = scrollView.contentOffset.y
// Reached the bottom of the list
if scrollPosition > bottom - buffer {
// Add more dates to the bottom
let lastDate = self.days.last!
let additionalDays = self.generateDays(
lastDate.dateFromDays(1),
endDate: lastDate.dateFromDays(self.daysToAdd)
)
self.days.append(contentsOf: additionalDays)
// Update the tableView
self.tableView.reloadData()
}
}
Here I am comparing the current scrollPosition
with the bottom
of the
UITableView in order to determine when I should add more dates. But instead of
adding more dates exactly when the user reaches the bottom, I use a
buffer
to add the dates to the list slightly before the bottom is
reached. This insures that we never actually hit the bottom of the UITableView
which would cause scrolling from a flick gesture to prematurely stop.
NOTE: The buffer
value is arbitrary and can be whatever you’d like. A larger
buffer
value will append additional content into the list earlier while a
smaller value will append additional content into the list closer to the
bottom of the UITableView. In situations where we are retrieving our content
over the network, a larger buffer
value is helpful in preventing the user from
reaching the bottom of the tableview before more content has been retrieved.
However, retrieving additional content too early can result in unnecessary
network requests if the user does not intend to scroll all the way down through
the already downloaded content. For this example, I’ve chosen to add more dates
to the list when I am 2 cells from the bottom of the list.
The last thing we have to do is refresh the table so that the UITableView
knows we’ve changed its underlying data.
Infinite scroll up
Scrolling infinitely up is basically the same as scrolling down except for one
important difference. A UITableView
’s contentOffset.y
is relative to the
top of the UITableView. So when we appended dates to the end of the list in the
previous example, the contentOffset
was not affected. But when we add dates
to the top of the list, the contentOffset
is not updated to compensate for
the newly added cells. This causes scrolling to jump and will result
in undesired behavior. But we can fix this! All we have to do is update the
contentOffset.y
by the height of a UITableView
cell (cellHeight
)
multiplied by the number of cells (daysToAdd
) we are prepending to the top of
the list.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// ...
// Reached the bottom of the list
if scrollPosition > bottom - buffer {
// ...
}
// Reach the top of the list
else if scrollPosition < top + buffer {
// Add more dates to the top
let firstDate = self.days.first!
let additionalDates = self.generateDays(
firstDate.dateFromDays(-self.daysToAdd),
endDate: firstDate.dateFromDays(-1)
)
self.days.insert(contentsOf: additionalDates, at: 0)
// Update the tableView and contentOffset
tableView.reloadData()
self.tableView.contentOffset.y += CGFloat(self.daysToAdd) * self.cellHeight
}
}
Great! We have successfully created infinite scrolling in both directions. What else could we do to make this better?
Window of Content
For that one user who will try to find the end of your content by scrolling up or down for days, let's protect ourselves from retaining all of this content in memory by beefing up our logic and only keeping a window of content. What I mean by "window of content" is that our table will only contain (let's say) 90 cells at a time. As we add new items to the bottom, we remove the same amount of items from the top and vice versa. Thus, it is like we are shifting a small window of content over the total available content.
To do this, we will now have to update the contentOffset
in both directions because (similar to the reasons as mentioned above) we are manipulating the top of the list each time we shift the window of dates.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// ...
// Reached the bottom of the list
if scrollPosition > bottom - buffer {
// ...
self.days.removeFirst(self.daysToAdd)
// Update the tableView and contentOffset
self.tableView.reloadData()
self.tableView.contentOffset.y -= CGFloat(self.daysToAdd) * self.cellHeight
}
// Reach the top of the list
else if scrollPosition < top + buffer {
// ...
self.days.removeLast(self.daysToAdd)
// Update the tableView and contentOffset
tableView.reloadData()
self.tableView.contentOffset.y += CGFloat(self.daysToAdd) * self.cellHeight
}
}
With this in place, we can now scroll forever in both directions and never have
to worry about excessive memory consumption. Checkout our final version here.
Conclusion
There are a couple of ways (as shown above) on how to implement infinite scrolling. Which approach you take depends on your situation as to whether you know all the possible content ahead of time or if you need to make requests over the network to get the available content. In most cases, your situation will probably be the latter and therefore I recommend implementing a refresh spinner at both ends of the UITableView so that in the event that the user reaches the end of the UITableView while a network request is being made to get more content, they will have a visual queue that something is happening and that more content is on its way!
Looking for more like this?
Sign up for our monthly newsletter to receive helpful articles, case studies, and stories from our team.
How to approach legacy API development
April 3, 2024Legacy APIs are complex, often incompatible, and challenging to maintain. MichiganLabs’ digital product consultants and developers share lessons learned for approaching legacy API development.
Read moreHow our Associates are using AI tools: Advice for early-career developers
August 13, 2024Our 2024 Associates at Michigan Labs share their experiences using AI tools like GitHub Copilot and ChatGPT in software development. They discuss how these tools have enhanced their productivity, the challenges they've faced, and provide advice for using AI effectively.
Read more