Infinitely scrolling tables

by Andy

One of the reasons I started my latest app, MealSchedule, was because I don’t care for presenting dates in blocks such as a week or a month. I don’t like that as you reach the end of the block a large proportion of the screen contains irrelevant historical data and I don’t like that it is cumbersome to compare items on either side of the block’s boundary. Time is continuous, therefore its display should be unbroken. Just because a week is nice and convenient for having one of each kind of day, you ignore that not every-one’s week starts on a Monday or a Sunday.

To translation a continuous time display to an app, you really need to use a vertical scroll as this is by far the most natural method for presenting an infinite amount of data. So the first challenge for my new app was to develop a way to handle and present an unlimited scrolling of dates.

The simplest option, of course, is to use a UITableView and return a very large number of rows. You can use scrollToRowAtIndexPath: to position today as the initial row, somewhere in the middle of the table. This works fairly well and is very easy to implement provided, and this is a big constraint, that each row is always the same height, indeed you do not want to implement tableView:heightForRowAtIndexPath: at all. iOS needs the height of each row to calculate the offsets of each cell within the scroll view of a table. When you call scrollToRowAtIndexPath: for example, iOS must determine the height of each preceding row, if that height is variable then the costs of calculating the offsets become untenable as soon as the table has more than a couple of hundred rows. With a fixed height, iOS will simply do the math to calculate the offset for a particular row.

But MealSchedule was going to have variable height content, so the simplest solution is off the table (pun intended!) But maybe we could be smarter and use a smaller number of rows as a window on to the underlying set of dates? Every time the user scrolls I could just change the dates that the visible rows represent.

A UITableView is a subclass of UIScrollView, so by implementing scrollViewDidEndDecelerating: and scrollViewDidEndDragging:willDecelerate: from the UITableView delegate – a table’s delegate serves up all of the UIScrollViewDelegate methods, I can use a quick sleight of hand to update the dates for the visible rows and reload the table. You have to implement both methods because either will be called depending on how the user scrolls. This pattern is a huge improvement over the first option, but suffers from a jerky appearance because with only a certain number of rows in the table there is an end point of the scrolling and there is a delay before I can update them.

While this second option was better there is still too much lag because of UITableView overhead in calculating row size. So the third approach is to strip out the overhead. UITableView is a complex class that takes care of a lot of situations, but I didn’t necessarily need all of that. What if I just had a UIScrollView and used by own UIViews to represent each day?

This proves to be a great solution. I could quickly add and remove subviews from the start and end of the content when the UIScrollView was scrolled and I could have variable height rows because I could control the number of times each row had to be recalculated. layoutSubviews is called every time a UIScrollView is scrolled, so I can quickly check to see if we are close to one end or the other, and if we are, remove the day subview from the opposite end, add in a new day and simply adjust the origins of the remaining subviews.

I choose to have a few extra days on either end of the visible set to allow for smoother scrolling. There is a function in the app to move to another date, and if that date is within the current set I can perform a nice animated scroll. But if the date is outside of the range, then I have to recalculate the entire set and there is no animated scroll. Most date changes are going to be by a week, so that is how many extra days I have available, just in case.

Here is the code in my UIScrollView subclass that handles the day subview creation and the scrolling (I have removed parts that are not relevant to this discussion.). The day subview is a UIView subclass called MealsDateView, the details of which are also irrelevant.

I have a category to extend NSDate which provides methods to convert between a full date and an integer as it is easier to work with a simple scalar in managing the set of days.