Fast UITableView Scrolling with Network Image Load
This is an old and common thing to do in an iOS app. Doing it well — or at all — it turns out, is pretty easy, but perhaps not obvious.
The Problem
You have a table view with table cells that each contain an image you retrieve from the internet. The problem is that for every image load, the table scrolling stalls or becomes jerky while the image is loaded because that load is happening on the main UI thread.
Result: Poor user experience.
First Solution
If you’re a developer with any experience or formal computer science training, the first solution that might come to mind is to load images in the background. That’s easy enough, but likely requires a custom UITableViewCell. In your custom cell implementation, you could write a method (called from tableView:cellForRowAtIndexPath: on the cell object) to load the image. The implementation of that method can then create a Grand Central Dispatch block or NSInvocationOperation or something to go get the image for the cell in the background.
This solution works a little better, but has a few problems. First, while now you have images loading in the background, you are still loading every image for every cell that is requested. This is bad in a “fast scrolling” situation, where someone has flicked your table in one direction very fast. Cells are appearing and disappearing much faster than their images can be fetched and displayed. Many GCD blocks or NSInvocationOperations are being queued, and executed! The effect is that the CPU is still spending a lot of time loading images that, now sadly, aren’t needed (because the cell for which the image was fetched has gone off screen (so to speak)), so the table scrolling is still jerky (albeit, probably less so). Worse, however, because table cells get re-used, you are likely to see a veritable slide show of images appear in the cells that ultimately do get displayed (once the table stops scrolling.)
Result: Probably a better user experience from a performance perspective, but still a less than optimal visual user experience as the images fill in and change in the ultimately visible table cells.
First Solution Tweak
The first solution isn’t entirely bad, per se. It takes advantage of background processing to accomplish tasks that would otherwise delay the main thread. Excellent! But other issues are then revealed through the slide-show appearance of images in the final cells and the still-jerky performance of the UI.
The slide show effect boils down to the fact that cells are re-used, but the background tasks initiated for each new use of a cell do not know when this happens. So a tweak to this solution is to add to the custom cell object a sequence number. When the cell is dequeued for (re-)use, it gets a new sequence number (taken from a global, monotonically increasing variable). When the background image load is occurring, the sequence number in the cell can be compared with the sequence number recorded when the image load was initiated. If they are different, the actual display of the image can be skipped.
Result: Only the last few cells dequeued or created and presumably visible on screen have their images actually displayed. However, every cell ever requested still has a background task initiated to load the image. So the CPU problem remains.
Last Solution
The solutions to this point have not been entirely off base. It is “a good thing” to load images for table cells in the background (or better, from a local cache if you loaded it once before), especially if they are coming off the internet because you cannot be assured that the user has a fast or good connection.
The actual thing to do, which makes so much sense it’s a wonder why people don’t think of this straight away (I didn’t!), is to only load images (for any cells) if the table view is not decelerating! That is to say, if the table view is scrolling, don’t load any cell images. And only when the table view stops scrolling (or rather, is not decelerating), load the images for the then visible cells.
So simple! How do we do it?
Remember that a UITableView is actually a descendent of UIScrollView. So it responds to the same UIScrollViewDelegate methods as would a first-class UIScrollView and has available to it the same properties you would expect to find on a UIScrollView.
First, in your implementation of tableView:cellForRowAtIndexPath:, you should include something like:
[code lang=”objc”]
…
if (!tableView.decelerating) {
NSString *url = …;
[cell showImageURL:url];
}
…
[/code]
The idea here is that on the initial display of your table, it will not be moving, so load the image (in the background, presumably). But during a fast scrolling scenario, or when the table is moving at all, image load will be skipped.
Next, you need to implement one of the UIScrollViewDelegate methods, something like this:
[code lang=”objc”]
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSArray *visibleCells = [self.tableView visibleCells];
[visibleCells enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
MyTableViewCell *cell = (MyTableViewCell *)obj;
NSString *url = …;
[cell showImageURL:url];
}];
}
[/code]
When the table stop moving completely, this method is called automatically for you. As you can see, only the visible table cells have their images loaded.
Result: Awesome user experience (your app performs really fast) and your app only does as much work as is needed to create that experience (loads images for only visible cells).
So simple! Yet, not necessarily obvious. But certainly another example of how Apple has thought of everything when it comes to the iOS SDK.
Awsome post. Recently, iOS 6.0 introduced some methods that make this even easier to tackle:
http://stavash.wordpress.com/2012/12/14/advanced-issues-asynchronous-uitableviewcell-content-loading-done-right/
There is a problem. The tableview just load when scroll is stopped!
Facebook load when you stop and when table view
Is scrolling! How?
It’s not a problem, it’s a choice. If you want your table view to scroll fast, all the time, then the thing to do is to only load the slow stuff (i.e. images from the network) when the table view is not scrolling. When you try to scroll the Facebook table, it will occasionally be jerky while something part of the UI is being changed (that likely need not be changed because it has scrolled out of view).
First of all, sorry if im boring you. I just really
Want to understand how they do that. Please
correct me if im saying something wrong cuz
im just a trainee( about 8mouths with iphone)
.I test just load images( with class sdwebimage )
visible exactly the post say and works very
Fast. I still want this effect but when scrolling
Fast. When make a low scroll or a fast scroll
and with low speed scrolling i also want to load
visible cells. I dont know if facebook do this
but when i scroll with facebook i didnt understand
what kind of actions they are handling.. But
They do something more of just load when
Tableview is stoped and it still scrolling fast
In addition to being able to know whether a table view is scrolling or not, you can also know how fast a table view is scrolling. So perhaps the Facebook app is deciding that once the table view is scrolling slow enough, it can start loading images. Or, maybe it’s not sophisticated at all. I couldn’t say, since I didn’t write it. A lot of learning and understanding comes from experimenting. If you want image loads (or whatever) while the table is scrolling, by all means go ahead and do that. It’s your code; you can do anything you want with it. 🙂
Oh! Man! You saved my time! Thank you!
Gosh what a great idea. I’m going to try this out!
Really cool and very helpful. As you said its a choice depending on your need. What I did was load a really low res version in cell for row at index path and then loaded the high res one when the scrollview was stationary. Thanks for the idea. Really helps in fast scroll situations for example scrolltocellatindexpath
Thanks for this. Very good idea and save my days 🙂