Monday, January 05, 2009

UITableView's numberOfRowsInSection behavior change from 2.0 to 2.2

Decided with iTimeZone 1.2 to upgrade and build against iPhone SDK 2.2. It seems like UIDatePicker is just *slightly* more reliable when configuring its timeZone property after it had been initialized. However, this resulted in an unintended side effect of The app crashing when removing all rows in the main city list table. It took me a couple hours to understand what was going on, but if you see the following error in console:

*** Assertion failure in -[CityTableView _endCellAnimations], /SourceCache/UIKit/UIKit-747.36.52/UITableView.m:679
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update must be equal to the number of rows contained in that section before the update, plus or minus the number of rows added or removed from that section.'

The cause, at least for me, is a mismatch in the expected return value of tableView:numberOfRowsInSection: that your code (most likely a UIViewController or UITableViewController instance) implements as part of the UITableViewDataSource protocol.

If your app is built against iPhone SDK 2.0 or 2.1, the correct behavior is to return 1 in tableView:numberOfRowsInSection: when you have no actual content and provide an empty cell in tableView:cellForRowAtIndexPath:. This does not cause tableView:deleteRowsAtIndexPaths to throw an exception when removing the last row.

If your app is built against iPhone SDK 2.2, the correct behavior is to return 0 in tableView:numberOfRowsInSection: when you have no actual content. tableView:cellForRowAtIndexPath: is never called. When your code calls tableView:deleteRowsAtIndexPaths as part of a row delete operation, everything works fine instead of crashing. An alternative correct behavior, if a UITableCell does something special (e.g. add a row, load more rows) is to always add 1 to the actual count of the items in the, most likely, NSMutableArray and return that in tableView:numberOfRowsInSection:, taken correct conditional action in tableView:cellForRowAtIndexPath: for putting your special cell at the top or bottom of the UITableView.

Hopefully you have come upon this article or figured this out on your own before submitting the latest release of your app to the App Store and got rejected for having a crasher in a main use case, just like, uh, someone I know did because no testers found the issue and you didn't rerun all use cases yourself after building against a new SDK version ;-)