Thursday, September 22, 2011

How to push and pop from the stack; bad access and trapping zombie calls

I'm having a problem with the NextLevelController. For some reason the "pop" method isn't working. It's just not popping back to the Start Controller when the button is pressed. But, the exact same functionality works for a different - going to the settings view and back. The only difference is how they're called. One (settings) is the result of a button push. The other is if a certain condition is met in the viewDidLoad method. What if I added a button to the StartController to see if that would work?

Well, I tried that, but still no luck. The method is not getting called, although I connected to the button.

Btw, here's how to do text alignment for the next time I notice that problem.

// align display labels
pctCorrect.textAlignment = UITextAlignmentRight;
correctSoFar.textAlignment = UITextAlignmentRight;
remaining.textAlignment = UITextAlignmentRight;
questionsAsked.textAlignment = UITextAlignmentRight;

Ah, ok, - i just accidentally hooked it up to the wrong button event. Ok, let's test the flow.

Ok - so far so good. Now, I want to make the "back" button disappear.

Good, it looks like I can do it in the "viewWillAppear" method:

- (void) viewWillAppear:(BOOL)animated{
self.navigationItem.hidesBackButton = YES;
}

Nothings ever easy - that caused the image to disappear! Do I have to call super or something?

Well, now it doesn't - but on the second go-round I get a bad access:


[self.navigationController popViewControllerAnimated:YES];

I believe I read somewhere that the "self.navigation" controller actually points to the navigation controller created in the app delegate, or the MainWindow.xlb to be precise. It's inherited through the view hierarchy, though I'd have to check that.

So, according SO, when a released object which isn't set to nil is sent a message, it will give you the bad access message.

I'm going to try this NSZombie trick from SO:

NSZombie will tell you what released object is being sent a message (aka EXC_BAD_ACCESS). This is a very useful tool when you get EXC_BAD_ACCESS, so learn how to use it.

To activate NSZombie do the following:

Get info of the executable.
Go to the arguments tab.
In the "Variables to be set in the environment:" section add:

Name: NSZombieEnabled Value: YES

Then run your app as usual and when it crashes it should tell you which deallocated object received the message.


Here's a better description of how to enable it:

http://www.cocoadev.com/index.pl?NSZombieEnabled

Use in Xcode:

Double-click an executable in the Executables group of your Xcode project.
Click the Arguments tab.
In the "Variables to be set in the environment:" section, make a variable called "NSZombieEnabled" and set its value to "YES".


Here's an even better one from SO:



In Xcode 4:

Press ⌥⌘R
From the tab "Info | Arguments | Diagnostics" select Diagnostics and click "Enable Zombie Objects".

From now on, released objects will turn into zombies and will appear in the debugger stack trace.

Pressing ⌥ ⌘R is the menu shortcut for selecting Product, keeping alt pressed, and clicking "Run...".
Clicking "Enable Zombie Objects" is the same as manually adding NSZombieEnabled YES in the section "Environment Variables" of the tab Arguments. Note that these Xcode settings are not used when you archive the application for App Store submission, so don't worry about submitting zombie infected apps.


Actually, I didn't see that option, so I added the environment variable as described above and shown below:





So, let's try it out.

Here's the message:

2011-09-22 17:51:28.182 JlptQuizApp[1971:207] *** -[UIImage isKindOfClass:]: message sent to deallocated instance 0x4e9ac20

Wow, that was really helpful. I had no idea. So, it's something to do with the image being displayed. Probably, something trying to send it a message once the pop happens?

Let's comment out the release and see what happens.

UIImage *img = [ UIImage imageWithContentsOfFile: imagePath];

if (img != nil) { // Image was loaded successfully.
[imageView setImage:img];
[imageView setUserInteractionEnabled:NO];
//[img release]; // Release the image now that we have a UIImageView that contains it.
}



Well, now, instead if crashing, it's just freezing, or staying put, when I press the next button. Foolish thing. Why? What have I done to offend it?

Ah - there's an important thing going on here. It's showing the same image on the second time through. That means either it's not hitting the randomization code, which it should, or I'm not handling the image right. My guess is the 2nd but let's check.

No - I set a breakpoint on it, and it definitely didn't hit it.

Ok, so I think what I want to do is either use the view will appear instead of the view did load, or maybe just clean it up when it returns from that view. I'm going to play it conservative and just destroy the view. I wonder why the question view doesn't have that problem?

Let's just quickly check on the "viewWillAppear".

Oops - it's complicated. I'm supposed to read the UIViewController's guide - but I'm sleepy enough right now as it is.

The problem is, I don't really know what's causing the problem. But I do know that it's not going through the viewDidLoad. So, popping the controller off the stack isn't enough to get rid of it.

This post

http://forums.macrumors.com/archive/index.php/t-510787.html

Seems to indicate that the dealloc method should be called:


When you push a view controller, it is retained by the parent view. Likewise, when you pop the view controller, it is released by the parent. Therefore, popping the view controller will dealloc it if and only if its retain count is 0 after the pop.


So there must be something hanging on to it, otherwise it would be deallocated, right?

Ok, well, if I release the object after pushing it:

Well, I'm having a world of trouble with retain counts going below zero.

Ok, here's the key: the object has to be reallocated and initialized *each time* you call it. Then you push it onto the nav controller, and release it. The nav controller will retain it, and then when you pop it, the retain count will go to zero. This gets you the call to loadView that you need; and you aren't stuck with an old stale version.

Here's the code:



@interface StartController : UIViewController {

QuestionController *questionController;
SettingsController *settingsController;
NextLevelViewController *nextLevelViewController;
NextLevelController *nextLevelController;

}

// name the controller as a property

@property (nonatomic, retain) IBOutlet QuestionController *questionController;
@property (nonatomic, retain) IBOutlet SettingsController *settingsController;
@property (nonatomic, retain) IBOutlet NextLevelViewController *nextLevelViewController;


- (IBAction)openSettingsController
{
NSLog(@"openSettingsController");


settingsController = [[SettingsController alloc] init];

settingsController.appState = appState;

[self.navigationController pushViewController:settingsController animated:YES];

[settingsController release];
}



And in the settings controller, when the button to return / save gets pressed:


[self.navigationController popViewControllerAnimated:YES];


That's how you do it!

No comments:

Post a Comment