Oct 11
SeattleBus Diary: One thousand eighteen bus stops
One thousand eighteen bus stops
… or how [UITableView reloadData] kicked my butt
The SeattleBus application includes a database with 1018 stops in it. Those were all the stops that I had with geolocation data and to which the MyBus web service responded with stop information. I started with 1211 data points, but just around 15% of the bus stops didn’t return any data.
One of the views in SeattleBus is a list of all the stops – and that’s where I felt the pain. In fact, it wasn’t until after 1.0 shipped that I realized exactly where the pain was coming from. It isn’t interesting caching or slow lookups from SQLite – it was from invoking UITableView reloadData where the data had 1000+ elements. Loading 100 or even 300 was a pretty reasonable delay. You’d notice it, but it wasn’t too bad. 1000 just pushed it over the edge to a good few seconds. Not to mention the UI component of trying to scroll through 1000 items on the iPhone. Man, that’s just painful. It screamed for (and I implemented) a search mechanism to make it a more useful view.
At WWDC this year, they warned us that reloadData was expensive. Ooooh yeah. One of the suggestions that I heard for resolving that was to load a subset of the data (say 100 rows, which isn’t too bad of a wait) and then using the method UITableView insertRowsAtIndexPaths:withRowAnimation: to inject additional rows using a background thread of when the user gets to where they need them. When I’d do a search and get back anywhere from 8 to 100 items (typical search in my tests), the reload was nicely fast. It was when I cancelled the search and reloaded the original view that everything went to hell. After reseting my model objects like a good programmer, I’d invoke [UITableView reloadData] like I was used to doing on a desktop Mac’s NSTableView. Then (on the device – the simulator was plenty fast), I’d wait.
Kris Markel came up with an interesting solution when cobbling together a search example for me (now That’s a beta tester!). Instead of preloading the data he was loading it in from the database in the UITableView data source method tableView:cellForRowAtIndexPath:. Ahhhh, right. I didn’t follow that path exactly (although I was darned tempted to), but instead implemented caching so that once I did that particular load, I didn’t have to re-do it again and again. I’ve got the application set to blow that cache away on a low memory warning, but it’s made a lot difference.
The next step there will be to load a subset up front and then populate that cache in the background with NSOperation or something equally interesting for a background thread. There’s no reason the user should have to wait through the 2.7 seconds of loading time for the whole kit.

March 18th, 2009 at 1:40 pm
I’ve been fighting my own similar performance battle lately, so this was extremely helpful reading! Sometimes I think we desktop Mac developers are actually at a disadvantage in the iPhone world, since we’re so accustomed to the normal “right” way to do things. Thanks for writing that up, Joe!
March 18th, 2009 at 1:51 pm
All props go to Kris Markel on this one – he’s the one who I first heard the solution from. I was more than happy to write it up – I figured someone else would run into the same issue I did. Glad it was useful!
January 5th, 2010 at 1:18 pm
Do you have a custom cell height? Are you defining it with tableView:heightForRowAtIndexPath: in your DataSource? Apparently, that is a horribly slow method. If you keep all cell heights the same and use the property rowHeight – you’ll find that reloadData is fast, even with thousands of rows.
I blogged about this problem:
http://www.xinsight.ca/blog/a-long-road-to-a-simple-solution/