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.
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!
LikeLike
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!
LikeLike
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/
LikeLike