Is there a reason I can't initialize a CALayer outside a UIView class?
Question
I have been trying to us CALayers as "sprites" in an iPhone application I'm working on. From what I have been reading, that seems like the correct thing to do here.
When I setup the layer inside a UIView class I am able to get the layer to show up on the screen: NSLog(@"GameView initWithCoder");
NSString* imagePath = [[NSBundle mainBundle] pathForResource:@"earth" ofType:@"png"];
earthImage = [[UIImage alloc] initWithContentsOfFile:imagePath];
earthLayer = [CALayer layer];
[earthLayer setContents:(id)[earthImage CGImage]];
[earthLayer setBounds:CGRectMake(0, 0, 32, 32)];
[earthLayer setPosition:CGPointMake(50, 50)];
[earthLayer setName:@"earth"];
[[self layer] addSublayer:earthLayer];
However, when I attempt to initialize the CALayer outside the UIView, my application crashes:
- (void)loadImage:(NSString*)fileName
{
NSLog(@"GamePiece loadImage");
NSArray* fileNameParts = [fileName componentsSeparatedByString:@"."];
imageName = [[fileNameParts objectAtIndex:0] retain];
NSString* ext = [fileNameParts objectAtIndex:1];
NSString* imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:ext];
image = [[UIImage alloc] initWithContentsOfFile:imagePath];
layer = [CALayer layer];
[layer setContents:(id)[image CGImage]];
[layer setBounds:CGRectMake(0.0f, 0.0f, 32.0f, 32.0f)];
[layer setPosition:CGPointMake(50.0f, 50.0f)];
[layer setName:imageName];
[fileNameParts release], fileNameParts = nil;
[ext release], ext = nil;
}
NSLog(@"GameView initWithCoder");
earth = [[GamePiece alloc] init];
[earth loadImage:@"earth.png"];
[[self layer] addSublayer:[earth layer]];
Clearly I'm doing something wrong here, but I just don't know what that would be. I have read the Core Animation Programming Guide and also the iPhone Application Programming Guide, but they really don't seem to talk about how to initialize a CALayer other than in passing.
I just need a gentle nudge in the correct direction. I have been getting a pretty good understanding about everything else I have learned when it comes to programming the iPhone. I'm just having some issues with Graphics, Layers and Animation.
Thanks in advance!
Solution
You don't own the layer that you get from the layer
method, so you have to take ownership. Sometime after your method returns, the layer is being deallocated and that is causing your crash. To cure this:
layer = [[CALayer layer] retain]; // now we explicitly take ownership of this layer
Also, don't forget to release the layer since you own it now:
// in your dealloc method
[layer release];
You should review the Memory Management Programming Guide for Cocoa for more details on when you own objects and what your responsibilities are regarding memory management.
OTHER TIPS
Jason, thank you so much for responding! However your answer doesn't seem to have any effect on the outcome of my problem.
Actually, the answer "is" something related to memory management. Not the Layer, but the name I got from the NSArray after calling componentsSeparatedByString:. If I don't release the NSArray, the code works fine. So I need to make a copy of the NSString inside the array instead of using the object in the array.
I have to say that I haven't read the Memory Management Programming Guide for Cocoa as you suggest. However, I have read several other book that talked about the standard used in Cocoa for objects created and their retain counts. I have a fair understanding, but apparently, not as good as needed yet.
I really had a suspicion that I should copy the string out of the array, but when it worked in the UIView, I just assumed that it was correct. Problem was, I wasn't keeping it like I am inside my GamePiece object.
Thank you for at least getting me to look in the right direction! I was really at wits end with this code.
To do what you wanted, here is how you could change your code. In loadImage:
use this line to add the UIImage
you loaded into the earth
layer:
self.contents = image.CGImage;
CALayers
have a contents
property which you can set to a CGImage
rather than going through the trouble of using Core Graphics calls to draw the image into the layer's context.
Then in the parent UIView
subclass you can add the earth
to the view hierarchy like this:
[self.layer addSublayer:earth];
Note that earth
is itself a CALayer
with an image as its contents so I add it directly as a sublayer. The other thing I always forget to do is set the layer's frame
property. Otherwise its width and height will be zero and so it won't show.
In your code you created a layer but never added your layer to the view hierarchy. There is a tree if views (class UIView) and their subviews. The root of the tree is [UIWindow mainwindow] (notice that UIWindow is a subclass of UIView
. Each UIView
has a layer
property of type CALayer
which actually holds its visible content. UIView
is just a wrapper around CALayer
to add things like touch handling ability.
The important idea you are missing here is that for anything to be visible on the screen it has to be part of this tree of views with a UIWindow as its root and all of the views must be visible (i.e. have their hidden
property set to NO
) between the toplevel window and the layer in question. Only then will a layer be visible on the screen. So somewhere in your code you need to add your layer as a sublayer of something already visible by calling addSublayer:
.
In your case, I'm assuming earth
is an instance of a CALayer
and what you have done inside the loadImage:
method is create a new CALayer
(with a call to [CALayer layer]
) within the earth
layer but then you never added that new layer to anything so it never became visible. What you did instead is attempt add the earth
layer, which still doesn't have any content, to what I assume is a UIView
's layer
[[self layer] addSublayer:[earth layer]];
One problem with this line is you seem to be thinking layer
is a property of earth
. Actually layer
is a class method of CALayer
which is a factory method to create new instances of CALayer
. This is confusing because [self layer]
actually is accessing the layer property of a UIView
instance. (I'm assuming self
refers to an instance of a UIView
subclass.)