"Swapping" a UIView Instance variable - cannot dealloc "previous" view - iphone

I want to organize somehow my iPhone game's level-views, but I simply cannot (without expanding Object Allocations). I made a really "skeleton" of my code (this game has 2 levels, the goal is to release the iPhone display). I just cannot dealloc the previous level, so Instrunments shows incrementing BGTangramLevel instances.
Please, take a look on it, I need some helpful ideas on designing (my 3rd question on it).
viewcontroller.h
#interface compactTangramViewController : UIViewController
{
//The level.
BGTangramLevel *currentLevel;
UIColor *levelColor;
}
//It is to be just a reference, therefore I use assign here.
#property (nonatomic, retain) BGTangramLevel *currentLevel;
-(void) notificationHandler: (NSNotification*) notification;
-(void) finishedCurrentLevel;
#end
viewcontroller.m
#implementation compactTangramViewController
#synthesize currentLevel;
//Initializer functions, setting up view hierarchy.
-(void) viewDidLoad
{
//Set up levelstepper.
levelColor = [UIColor greenColor];
//Set up "state" classes.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(notificationHandler:) name:#"finishedCurrentLevel" object:nil];
//Attach level 1.
currentLevel = [BGTangramLevel levelWithColor: levelColor frame:self.view.frame];
[self.view addSubview:currentLevel];
[super viewDidLoad];
}
//Release objects.
-(void) dealloc
{
[currentLevel release];
[super dealloc];
}
//Notification handling.
-(void) notificationHandler: (NSNotification*) notification
{
//Execute level swap.
if ([notification name] == #"finishedCurrentLevel") [self finishedCurrentLevel];
}
-(void) finishedCurrentLevel
{
//Remove previous level.
[currentLevel removeFromSuperview];
//[currentLevel release];
//Step level.
if (levelColor == [UIColor greenColor]) levelColor = [UIColor blueColor]; else levelColor = [UIColor greenColor];
//Attach level 2.
currentLevel = [BGTangramLevel levelWithColor: levelColor frame:self.view.frame];
[self.view addSubview:currentLevel];
}
#end
BGTangramLevel.h
#interface BGTangramLevel : UIView
{
BOOL puzzleCompleted;
}
//Initializer.
+(BGTangramLevel*)levelWithColor: (UIColor*) color frame: (CGRect) frame;
//Test if the puzzle is completed.
-(void) isSolved;
#end
BGTangramLevel.m
#implementation BGTangramLevel
//Allocated instance.
+(BGTangramLevel*)levelWithColor: (UIColor*) color frame: (CGRect) frame
{
BGTangramLevel *allocatedLevel = [[BGTangramLevel alloc] initWithFrame:frame];
allocatedLevel.backgroundColor = color;
return allocatedLevel;
}
//Finger released.
-(void) touchesEnded: (NSSet*)touches withEvent: (UIEvent*)event
{
//The completement condition is a simple released tap for now...
puzzleCompleted = YES;
[self isSolved];
}
//Test if the puzzle is completed.
-(void) isSolved
{
//"Notify" viewController if puzzle has solved.
if (puzzleCompleted) [[NSNotificationCenter defaultCenter] postNotificationName:#"finishedCurrentLevel" object:nil];
}
-(void) dealloc
{
NSLog(#"Will ever level dealloc invoked."); //It is not.
[super dealloc];
}
#end
So what should I do? I tried to mark autorelease the returning level instance, release currentLevel after removeFromSuperview, tried currentLevel property synthesized in (nonatomic, assign) way, but Object Allocations still grow. May I avoid Notifications? I'm stuck.

You need to follow retain/release rules more closely. You definitely should not experimentally add retain and release and autorelease in places just to find something that works. There's plenty written about Cocoa memory management already, I won't repeat it here.
Specifically, BGTangramLevel's levelWithColor:frame: method should be calling [allocatedLevel autorelease] before returning allocatedLevel to its caller. It doesn't own the object, it's up to the caller to retain it.
You also need to know the difference between accessing an instance variable and accessing a property. Cocoa's properties are just syntactic-sugar for getter and setter methods. When you reference currentLevel in your view controller you are dealing with the instance variable directly. When you reference self.currentLevel you are dealing with the property.
Even though you've declared a property, currentLevel = [BGTangram ...] simply copies a reference into the variable. In viewDidLoad, you need to use self.currentLevel = [BGTangram ...] if you want to go through the property's setter method, which will retain the object (because you declared the property that way). See the difference?
I think your leak is happening in finishedCurrentLevel. If you had used self.currentLevel = [BGTangram ...], the property's setter method would be called, which would release the old object and retain the new one. Because you assign to the instance variable directly, you simply overwrite the reference to the old level without releasing it.
Calling [currentLevel release] in the dealloc method of your view controller is correct.

Related

objective-c memory management clarification

I am trying to make a simple iphone app and I have been playing around with features and recently, delegates. I am just confused with regards to memory management because apparently "good code" makes my app crash with exc_bad_access.
I have an object with two data members and implementation empty for now.
#implementation semester: NSObject{
NSInteger ID;
NSString *name;
}
then my delegate method:
- (void) receiveSemester:(semester *)newSemester {
[test setText:newSemester.name];
}
and a view that is used as a form which has:
#interface addSemesterController : UIViewController {
id<ModalViewDelegate> delegate;
UITextField *txtName;
UILabel *prompt;
UIButton *ok;
UIButton *cancel;
}
all objects are made properties and synthesized in the application file. Here is the method that used the delegate:
- (IBAction) okClick:(id)sender{
// create semester object and return it
semester *created = [[semester alloc] init];
created.name = txtName.text;
[delegate receiveSemester:created];
[self dismissModalViewControllerAnimated:YES];
}
And my dealloc method looks like this:
- (void)dealloc {
/*
[txtName dealloc];
[prompt dealloc];
[ok dealloc];
[cancel dealloc];
*/
[super dealloc];
}
With the deallocs of the objects contained in the form commented out, my app runs ok. However, when I uncomment them, I receive the exc_bad_access error in my delegate protocol:
// in main view controller
- (void) receiveSemester:(semester *)newSemester {
[test setText:newSemester.name];
// test is a UILabel
}
I tried the zombie method and it says that the label calls a released object. I am not releasing my semester object in the "form" controller, and even if I was the delegate function is called before deallocating the view.
Clearly I should not be releasing the objects in the dealloc method, I am just unclear in the why I shouldn't.
Again, thanks in advance for the help.
Use release to release the variables instead of calling dealloc on variables, due to this you are having issue -
- (void)dealloc {
[txtName release];
[prompt release];
[ok release];
[cancel release];
[super dealloc];
}
try writing
[txtName release];
[prompt release];
[ok release];
[cancel release];
instead of dealloc and these objects will be deallocated properly

objective-c Novice - Needs help with changing outlets in different classes

My main class MMAppViewController has an "IBOutlet UIImageView *padlock". This controller pushes a Level1View view which is my quiz game. The MMAppViewContoller has 2 buttons level 1 and level 2. Level 2 has the padlock on it and will unlock when a certain score is reached. When the MMAppViewController is pushed back, is there a way to hide the padlock. I know the following code will do this but my problem lies in where to put the code:
if(theScore>4){
[padlock setHidden:TRUE];
}
With my Level1View i can put code in the "viewdidload()" section, but it does not work with my main view because it only seems to load once! I tried puting the code in my Level1View class but keep getting errors about tokens or it being undeclared:
[MMAppViewController padlock setHidden:TRUE];
or
[padlock setHidden:TRUE];
Is there a way of either putting this code in my Level1View class, or is there a way of having the code in my MMAppViewContoller class that will work when Level1View is "unpushed"?? (not sure of terminology)
Not knowing more about the structure of your program it's hard to know the right way to achieve this.
There are several possible approaches, but viewDidLoad is only going to be called once and should be used for setting up the view initially, and not for this sort of repeated logic. You probably have a model object somewhere that holds the score. (If you don't, i.e. if theScore is an instance variable on your ViewController, as your snippets might imply, you should move it to it's own model object.) The best way to go about this would be for your ViewController to "observe" the model object that holds the score using Key-Value Observing. Here's how you might achieve that:
Let's say you have the following model object to hold your game session data (here, only the current score):
#interface GameSession : NSObject
#property (readwrite) double score;
#end
... and its corresponding implementation ...
#implementation GameSession
#synthesize score;
#end
And then assuming you have a ViewController declaration that looks something like this:
#class GameSession;
#interface MyViewController : UIViewController
{
GameSession *game;
IBOutlet UIImageView *padlock;
}
#end
You could set up the following methods on the ViewController, such that every time the score value of the model object is modified, the ViewController will automatically update the hidden state of the padlock image view:
- (void)viewDidLoad
{
[super viewDidLoad];
game = [[GameSession alloc] init];
[game addObserver:self forKeyPath:#"score" options:NSKeyValueObservingOptionInitial context: [RootViewController class]];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == [RootViewController class])
{
if ([keyPath isEqualToString: #"score"])
{
NSNumber* newValue = [change objectForKey: NSKeyValueChangeNewKey];
double currentScore = [newValue doubleValue];
[padlock setHidden: (currentScore < 4.)];
}
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
[game removeObserver:self forKeyPath:#"score"];
[game release];
game = nil;
[super dealloc];
}
For a full explanation of Key-Value Observing, see this web page: http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/KeyValueObserving/
Let me know if this isn't clear.
The simple option is to put the code in viewWillAppear:.

Removing views from UIScrollView

Using the PhotoScroller example from Apple, to reuse the memory allocated for views, I cannot get the memory released once the retain count hits 0. Here my code for a better understanding:
This piece is an extract from PhotoScroller
PhotoViewController.m
- (void)configurePage:(ImageScrollView *)page forIndex:(NSUInteger)index
{
page.index = index;
page.frame = [self frameForPageAtIndex:index];
[page displayPage:index];
}
ImageScrollView.h
#interface ImageScrollView : UIView
{
UIViewController *vc;
NSUInteger index;
}
#property (assign) NSUInteger index;
- (void)displayPage:(int)indice;
#end
ImageScrollView.m
- (void)dealloc
{
[vc release];
[super dealloc];
}
- (void)displayPage:(int)indice
{
//remove previous view
[vc.view removeFromSuperview];
[vc release];
vc = nil;
//NSLog(#"vc retain %i", [vc retainCount]);
// make a new viewController for the new page
Class clss = NSClassFromString([NSString stringWithFormat:#"page_0%i", indice + 1]);
vc = [[clss alloc] initWithNibName:[NSString stringWithFormat:#"page_0%i", indice + 1] bundle:nil];
[self addSubview:vc.view];
}
The classes "page_0x" are UIViewControllers. So far have nothing but a UIImageView on the XIB.
An example of page_01:
#implementation page_01
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Overriden to allow any orientation.
return YES;
}
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
// Release any cached data, images, etc that aren't in use.
}
- (void)viewDidUnload {
[super viewDidUnload];
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (void)dealloc {
[super dealloc];
}
#end
Memory peaks to 148 MB on device. Gives me a memory warning Level 1, then releases some memory. Goes down to 95 MB. Keeps going up and down.
Perhaps your page_0x view controllers are getting freed, but something they've created isn't. Some things to check:
Set a breakpoint in the dealloc method of your page_0x view controllers. Is it getting called?
Check all the IBOutlets and other instance variables of your page_0x view controllers. Are they all being released properly in their class's dealloc method?
Run Build & Analyze. Does it turn up anything?
Try running the Leaks Instrument. It can tell you what kind of objects are actually leaking.
(EDIT) Grasping at straws now. You don't have NSZombieEnabled turned on, do you?
(EDIT 2) You say you're throwing a .png on it. What happens if you remove that .png?
If you're running in the simulator, try it on the device. (See this question.)

iphone using cocos2d get EXC_BAD_ACCESS

I am making a simple game project involving the use of Cocos2d. Now as defined by Ray Wenderlich's example, i have completed the whole tutorial but added an extra bit of code myself to check total number of melons, when they reach 3, i replace screen with "You Win" screen to notify the user that he has won using [[CCDirector sharedDirector] replaceScene:gameoverscreen];.
The problem is that i get EXC_BAD_ACCESS everytime i call this from ccTouchEnded coz my condition is checked here. But the same thing works if i use [[CCDirector sharedDirector] pushScene:gameoverscreen];
Cant understand what the problem is!!
the code for gameoverscreen screen is:
#import "GameOverScene.h"
#import "HelloWorldScene.h"
#implementation GameOverScene
#synthesize _layer = layer;
- (id)init {
if ((self = [super init])) {
self._layer = [GameOverLayer node];
[self addChild:layer];
}
return self;
}
- (void)dealloc {
[layer release];
layer = nil;
[super dealloc];
}
#end
#implementation GameOverLayer
#synthesize _label = label;
-(id) init
{
if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {
CGSize winSize = [[CCDirector sharedDirector] winSize];
self._label = [CCLabel labelWithString:#"" fontName:#"Arial" fontSize:32];
label.color = ccc3(0,0,0);
label.position = ccp(winSize.width/2, winSize.height/2);
[self addChild:label];
[self runAction:[CCSequence actions:
[CCDelayTime actionWithDuration:3],
[CCCallFunc actionWithTarget:self selector:#selector(gameOverDone)],
nil]];
}
return self;
}
- (void)gameOverDone {
[[CCDirector sharedDirector] replaceScene:[[[HelloWorld alloc] init] autorelease]];
}
- (void)dealloc {
[label release];
label = nil;
[super dealloc];
}
#end
and the Header file of GameoverScene contains the following!
#import "cocos2d.h"
#interface GameOverLayer : CCColorLayer {
CCLabel *label;
}
#property (nonatomic, retain) CCLabel *_label;
#end
#interface GameOverScene : CCScene {
GameOverLayer *layer;
}
#property (nonatomic, retain) GameOverLayer *_layer;
#end
i call the scene from HelloWorld class using the following syntax!
GameOverScene *gameoverscene = [GameOverScene node];
[gameoverscene._layer._label setString:#"You WON!"];
[[CCDirector sharedDirector] pushScene:gameoverscene];
I see several issues in your code.
One is the CCLabel object, you initialize it as autorelease object using cocos2d's static initializer:
self._label = [CCLabel labelWithString:#"" fontName:#"Arial" fontSize:32];
But in the dealloc method you release it even though its an autorelease object:
- (void)dealloc {
[label release];
label = nil;
[super dealloc];
}
You should not release the label since it is set to autorelease by cocos2d! This is a guaranteed crash!
Then you make things more complicated than needed:
[[CCDirector sharedDirector] replaceScene:[[[HelloWorld alloc] init] autorelease]];
The alloc/init/autorelease is completely superfluous because you can simply write [HelloWorld scene] if the HelloWorld class has a +(id) scene method (it normally should). If not, then use [HelloWorld node]. Always prefer cocos2d's static autorelease initializers before using alloc/release on cocos2d objects. The only time you ever need to alloc a cocos2d class is when you explicitly don't add it as a child to some other node, which is rare.
Finally, this is very bad style:
-(id) init
{
if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {
If the super implementation of initWithColor calls [self init] - which is often the case and even if not, could change with future releases of cocos2d - it would call your implementation of init, resulting in an endless loop (stack overflow). To fix this simply either rename your init method or call [super init] and provide the parameters some other way, usually there will be a property or setter method to do so.
And a minor issue: Apple advises against using leading underscores as member variable prefix. In fact, many other compiler vendors advice against that too since often system internal variables use one or two underscores as prefix. The cocos2d style with trailing underscores is preferred which would have you write label_ instead of _label.
EXEC_BAD_ACCESS means you are using data that has been released. Does the youwin scene use data from the current scene? If so, it needs to retain the data. When replaceScene: is called, the current scene is not held in memory but when pushScene: is called, both scenes remain in memory.
EDIT:
Let's say you have two scenes, A and B. When you call pushScene:, A continues to exist in memory and B is added. When you call replaceScene:, A is removed and no longer exists, only the B scene. That is why A's data would disappear, but only when replacing.
The general rule when it comes to memory dealing is to release whatever you alloced or retained. In your case you are instantiating a CCLabel object with a convenience method (thus, not calling alloc) and you are not retaining it. So, that [label release] in your dealloc method must not be there in this case.
I also account with such thing,the reason for it may be you release something that are autorelease,so you can try it again by not to release some object in the dealloc method!

UIView and NSTimer not releasing memory

I have a line of UILabel text in a UIView which is regularly updated via NSTimer. This code is supposed to write a status item near the bottom of the screen every so often. The data comes from outside of its control.
My app runs out of memory really fast because it seems the UILabel is not being released. It seems that dealloc is never called.
Here is a very compressed version of my code (error checking etc removed for clarity.):
File:SbarLeakAppDelegate.h
#import <UIKit/UIKit.h>
#import "Status.h"
#interface SbarLeakAppDelegate : NSObject
{
UIWindow *window;
Model *model;
}
#end
File:SbarLeakAppDelegate.m
#import "SbarLeakAppDelegate.h"
#implementation SbarLeakAppDelegate
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
model=[Model sharedModel];
Status * st=[[Status alloc] initWithFrame:CGRectMake(0.0, 420.0, 320.0, 12.0)];
[window addSubview:st];
[st release];
[window makeKeyAndVisible];
}
- (void)dealloc
{
[window release];
[super dealloc];
}
#end
File:Status.h
#import <UIKit/UIKit.h>
#import "Model.h"
#interface Status : UIView
{
Model *model;
UILabel * title;
}
#end
File:Status.m
This is the where the problem lies. UILabel just does not seem to be released, and quite possible the string as well.
#import "Status.h"
#implementation Status
- (id)initWithFrame:(CGRect)frame
{
self=[super initWithFrame:frame];
model=[Model sharedModel];
[NSTimer scheduledTimerWithTimeInterval:.200 target:self selector:#selector(setNeedsDisplay) userInfo:nil repeats:YES];
return self;
}
- (void)drawRect:(CGRect)rect
{
title =[[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 12.0f)];
title.text = [NSString stringWithFormat:#"Tick %d", [model n]] ;
[self addSubview:title];
[title release];
}
- (void)dealloc
{
[super dealloc];
}
#end
File: Model.h (this and the next are the data sources, so included only for completeness.) All it does is update a counter every second.
#import <Foundation/Foundation.h>
#interface Model : NSObject
{
int n;
}
#property int n;
+(Model *) sharedModel;
-(void) inc;
#end
File: Model.m
#import "Model.h"
#implementation Model
static Model * sharedModel = nil;
+ (Model *) sharedModel
{
if (sharedModel == nil)
sharedModel = [[self alloc] init];
return sharedModel;
}
#synthesize n;
-(id) init
{
self=[super init];
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(inc) userInfo:nil repeats:YES];
return self;
}
-(void) inc
{
n++;
}
#end
The problem is that you are never removing the UILabel from the Status UIView. Let's take a look at your retain counts in drawRect:
(void)drawRect:(CGRect)rect {
title =[[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 12.0f)];
Here, you have created a UILabel with alloc, which creates an object with a retain count of 1.
[self addSubview:title];
[title release];
Adding the UILabel to Status view increases title's retain count to 2. The following release results in a final retain count of 1. Since the object is never removed from its superview, the object is never deallocated.
Essentially, you are adding one UILabel on top of another, each time the timer is fired, until memory runs out.
As suggested below, you should probably create the UILabel once when the view loads, and just update the UILabel's text with [model n].
As a housekeeping note, you might also want to make sure that you are properly deallocating any left over objects in your dealloc methods. 'model' and 'title' should be released in Status' dealloc, just as 'model' should be in SbarLeakAppDelegate.
Hope this helps.
Edit [1]:
It sounds like you have the memory issue pretty well handled at this point. I just wanted to suggest another alternative to the two timers you are using.
The timer you have running in your Status object fires every .2 seconds. The timer which actually increments the 'model' value, n, fires only once each second. While I believe you are doing this to ensure a more regular "refresh rate" of the Status view, you are potentially re-drawing the view 4 or 5 times per second without the data changing. While this is may not be noticeable because the view is fairly simple, you might want to consider something like NSNotification.
With NSNotification, you can have the Status object "observe" a particular kind of notification that will be fired by the Model whenever the value 'n' changes. (in this case, approximately 1 per second).
You can also specify a callback method to handle the notification when it is received. This way, you would only call -setNeedsDisplay when the model data was actually changed.
There are 2 problems with your code.
Problem 1
In -drawRect you add a subview to the view hierarchy every time the view is drawn. This is wrong for 2 reasons:
Every time the view is drawn, the number of subviews increases by 1
You are modifying the view hierarchy at draw time - this is incorrect.
Problem 2
Timers retain their targets. In the initializer for your Status object, you create a timer which targets self. Until the timer is invalidated, there is a retain cycle between the timer and the view, so the view will not be deallocated.
If the approach of using a timer to invalidate the view is really the correct solution to your problem, you need to take explicit steps to break the retain cycle.
One approach to doing this is to schedule the timer in -viewDidMoveToWindow: when the view is being put in a window [1], and invalidate the timer when the view is being removed from a window.
[1] Having the view invalidate itself periodically when it isn't displayed in any window is otherwise pointless.
Instead of calling -setNeedsDisplay with your NSTimer in the view controller, why not create a method that calls "title.text = [NSString stringWithFormat:#"Tick %d", [model n]] ;"? That way instead of re-creating the label every time the timer fires you can just update the value displayed.