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.

spinner_bottom

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
    }
}

spinner_bottom

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!