Grocophile iPhone icon

The Item Detail Bug

Finding The Problem

Although most bugs are very interesting, in the "annoying to our users" and "embarrassing to our developers" meanings of the word, most bug fixes are pretty boring. There's usually some error in the code that's brutally obvious once you realize what's going on: either there was some mistaken assumption, some unexpected circumstance or even just a dumb typo. This bug wasn't any of those.

Unlike today's desktop computers, the iPhone and iPod Touch have pretty limited memory in which programs have to store all of their working data when running. So, the operating system is pretty frugal with how it uses memory and is always looking for ways to toss out data that's not needed right now and can be rebuilt if needed later. For example, an application like Grocophile can have many views or screens of information open at the same time. Each of the four tabs has a stack of views, perhaps totaling a dozen views that the user has opened. Since only one of these views can be shown on screen at once, the operating system can choose to toss out all of the display structures associated with inactive views. When the user switches to another view, its display structures are instantly reconstructed for presentation to the user.

This is all well and good, but there are some implications to the programmer. First, the programmer can't depend on the display structures sticking around when a view becomes inactive, instead those data structures may be tossed out then rebuilt later. So, if you care about some aspect of those structures, you have to track when they get thrown out and rebuilt. That can lead to some subtle bugs and is part of the learning curve.

This bug was caused by a more subtle interaction. The circumstance of the bug is that you have started in the Inventory view and then moved to the item detail view to edit either a Group item or an Inventory item. The active view is the detail view, but underneath it is an Inventory view showing either the list of groups, or the list of items in a group. Now suppose the display structures for the underlying view have been thrown out of memory. When you complete an edit operation (changing the name, detail, or aisle assignment), the underlying view needs to be informed of the change so it knows to update the item's appearance to reflect the change. The code that handles this update knows that its display structures may have been tossed out of memory, so it first tells the operating system that it wants the display structures rebuilt and verifies that happened correctly. If all goes well, it makes sure that when the user returns to the Inventory view, the item that was just edited is visible, scrolling it onto the screen if necessary. If the structure controlling the scroll position of the list had been thrown out of memory, even though it has been rebuilt, trying to set the scroll position causes the application to crash.

That's the bug. It can happen one of two ways. First, if the program has been running for a while and the user has opened many views which are consuming a lot of memory, then the operating system decides to toss out inactive view data while the item detail view is open on a newly created item. Second, if you're looking at an item detail view on a new item when you quit Grocophile, when you start it again your full view state is restored but the display structures for inactive views (like the underlying Inventory list) have not yet been created by the operating system. In either of these circumstances, if you edit the item, Grocophile would crash.

This was hard for us to track down because it depended on the operating system choosing to throw out inactive display data at just the right time. We got reports from a tiny fraction of our users that they had spent a lot of time editing items then Grocophile crashed, but then it wouldn't happen again. We tried to do that and never managed to fall into one of the unusual circumstances.

Once we knew exactly what code was failing from a user's crash report, we figured out the problem by using the iPhone simulator and forcing one of those memory purges with the item detail view up. So, the lesson for iPhone developers and testers is to use the "Simulate Memory Warning" command in the iPhone simulator frequently. Once we had identified the problem, we realized that it could also be forced to happen in the start-up case, which explained one report of it happening right away after launching the app.

The Solution

The solution is to very carefully track when the display structures for the underlying inventory view get created and destroyed and then not even try to ensure the edited item is visible if the display structures are not currently in memory when the edit occurs.

Source Code

Here's the original code. It's in a method of a UITableViewController subclass and is called by a view pushed onto the view stack to edit an item shown in the table. The callback occurs whenever the item is changed.

  if (self.tableView)
  {
    [self.tableView scrollToRowAtIndexPath:
      [NSIndexPath indexPathForRow:rowNew inSection:0]
      atScrollPosition:UITableViewScrollPositionMiddle
      animated:YES];
  }

Referencing self.tableView actually calls UITableViewController's method [self tableView] which will return the tableView, creating it if needed. But for some reason, if the UITableView had to be created it's not really all there yet and crashes when scrollToRowAtIndexPath: is called.

The solution is to add a BOOL member variable to the UITableViewController subclass:

  BOOL tableIsLoaded;

Initialize tableIsLoaded to NO in your subclass' initWithNibName: or initWithStyle: method.

  tableIsLoaded = NO;

Subclass viewDidLoad and didReceiveMemoryWarning:

  - (void)viewDidLoad
  {
    tableIsLoaded = YES;
  }
  
  - (void)didReceiveMemoryWarning
  {
    // Releases the view if it doesn't have a superview
    tableIsLoaded = NO;
    [super didReceiveMemoryWarning];
  
    // Release anything that's not essential, such as cached data
  }

Finally, instead of forcing the UITableView to be created, just skip the code that fools around with the table's scroll position if the table isn't currently loaded:

  if (tableIsLoaded)
  {
    [self.tableView scrollToRowAtIndexPath:
      [NSIndexPath indexPathForRow:rowNew inSection:0]
      atScrollPosition:UITableViewScrollPositionMiddle
      animated:YES];
  }