views:

85

answers:

2

I have a view controller managed in a UINavigationController. My view controller puts up a "loading" screen, and uses ASIHTTP to asynchronously load table data. Then it renders its table, and in each cell there's an image that it uses ASIHTTP to asynchronously load. When it lands each image, it uses [self.tableView reloadRowsAtIndexPaths] to reload that row, inside which the image is fed to the UIImageView in each row.

Works great. But if I back out of the view before it's done loading, it crashes.

Where it crashes is in my -tableView:numberOfRowsInSection method, and NSZombies tells me it dies because it's asking for the -count of an NSArray called self.offers that has been deallocated.

That method looks like this:

-(NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section
{
 return [self.offers count];
}

Wrapping that return in if (self.offers) made no difference.

My -dealloc releases and sets-to-nil every one of these properties, including both self.offers and self.tableView itself. I even tried setting up a BOOL disappearing property, hitting it with YES in -viewWillDisappear, and hanging conditional behavior off that, but it doesn't work because viewWillDisappear doesn't seem to get called! Far as I can tell we're not getting ANY method called when the navigation bar pops us off.

What do I do about this?

EDIT:

Thanks to @cduhn (who's bucking for a check!), I did a bunch more looking at this. The problem has been, my -dealloc just isn't getting called when I pop this viewcontroller (nor my -viewWillDisappear nor -viewDidUnload or anything else I could use to unhook the delegation structure that's at the root of this problem).

Then I realized: THIS viewController isn't the one on the NavController stack! What's at the top of the stack right here is a shell view, just a segmented controller and a big empty UIView. I toggle the contents of that UIView between two other UIViewController subclasses depending on the state of my segmented controller. So when my view with the table on it's PARENT view gets popped from the nav stack, this CHILD I'm working on doesn't seem to get any notice about it. Which is odd, because I'm definitely releaseing it.

I can call its -dealloc from my shell controller. I could call its -viewWillDisappear too, for that matter. Is that how I should be handling this? Probably I should put something into my shell controller's viewWillDisappear like:

[[self.mainView subviews] makeObjectsPerformSelector:@selector(viewWillDisappear)];

...so that message propagates down to my child views.

Am I on the right track here, you think??

(Oh man... and that also explains why actions from inside this child table view can't get to self.navigationController! I've been puzzled about that for weeks!)

A: 

As far as I can tell, the user cannot actually "back out" of a view while the UITableView is loading it's data. The methods are not run on a thread and block the main one, also blocking UI interaction. I cannot replicate your results. Even, scrolling the table view quickly and then pressing the back button.

I suggest that the stack popping is not the problem here.

cool_me5000
The stack trace is pretty unambiguous about this. We're definitely dying inside `-tableView: numberOfRowsInSection:`. And definitely only when I've backed out while the table is reloading. It could be that the backing-out happens during the window of the ASIHTTP request doing its business, but I can't seem to make it sensitive to its state enough to do anything smart.
Dan Ray
+2  A: 

Bugs like this, where a method gets called on an object after it's been deallocated, often happen when a method gets called on a delegate after that delegate has been deallocated. The recommended practice to avoid bugs like these is to set any delegate (or delegate-like) properties of an object to nil before you release that object in the delegate's dealloc method. I know that's a confusing sentence, so I'll explain it in the context of your bug.

You have an asynchronous image download that finishes after you've backed out of your table view controller. When this happens, you're calling reloadRowsAtIndexPaths:withRowAnimation:, which results in a call to tableView:numberOfRowsInSection: on the table view's dataSource. This call is failing because that dataSource no longer exists.

The problem is that the table view object still has your controller set as its dataSource and delegate properties, even after your controller has been deallocated. The solution is to simply set these properties to nil in your controller's dealloc, like this:

- (void)dealloc {
    self.tableView.dataSource = nil;
    self.tableView.delegate = nil;
    self.tableView = nil; // Releases as a side-effect due to @property (retain)
    [super dealloc];
}

Now when your table view tries to call tableView:numberOfRowsInSection: on its dataSource, it will send the message to the nil object, which swallows all messages silently in Objective C.

You should also do the same thing with your ASIHTTPRequest's delegate, by the way.

Any time you have an asynchronous operation that calls delegate methods upon completion, it's particularly important that you set that delegate to nil. When using UITableViewController under normal circumstances you can typically get away without setting the dataSource and delegate to nil, but in this case it was necessary because you're calling methods on the tableView after its controller has gone away.

cduhn
Wow. Okay. Yes, I'm with you about all of this. Please see my edit to the OP for some serious re-focusing of this question.
Dan Ray