UIImage content disappearing (and reappearing) when using drawLayer:inContext - objective-c

I'm using the delegate method drawLayer:inContext to change the background opacity for a small box as it moves around the screen. The layer had a UIImage set as the content when it was initialized.
Every time drawLayer:inContext is called the image will either disappear or reappear (toggling back and forth) making for a stutter-y looking movement on screen.
I have the image data as a local ivar in the delegate class so that it doesn't need to reload every time.
I'm not sure what's causing this. Any ideas? Here's the code I'm using:
- (id)init
{
self = [super init];
if(self) {
UIImage *layerImage = [UIImage imageNamed:#"Hypno.png"];
image = layerImage.CGImage;
}
return self;
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
float alpha = layer.position.y / layer.superlayer.bounds.size.height;
UIColor *reddish = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:alpha];
CGColorRef cgReddish = reddish.CGColor;
layer.backgroundColor = cgReddish;
[layer setContents:(id)image];
}

Don't use -drawLayer:inContext:. That method is for drawing into a CG context which will then become the layer's contents, using Core Graphics or UIKit methods. You don't want to change the layer's contents at all -- you want to set it to be your image, once, and then never touch it again.
Your image is appearing and disappearing, because sometimes the layer displays the image, and other times the layer displays what you drew into the context in -drawLayer:inContext: -- namely, nothing.
(Also: in order for -drawLayer:inContext: to be called, you must be calling [layer setNeedsDisplay], or you have the layer's needsDisplayOnBoundsChange turned on, or something similar. Don't do any of that.)
Instead, try using the CALayer layout machinery. For instance, in your layer's delegate:
- (void)layoutSublayersOfLayer:(CALayer *)layer
{
float alpha = layer.position.y / layer.superlayer.bounds.size.height;
UIColor *reddish = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:alpha];
CGColorRef cgReddish = reddish.CGColor;
layer.backgroundColor = cgReddish;
}
This will get called as part of the display process, anytime the layer or any of its superlayers have had their -setNeedsLayout method called. Often this happens automatically, but depending on exactly what causes the layer to move, you may need to do it manually. Try it and see.
(Alternatively: Since you already have code that changes the layer's position, why not change the layer's background color at the same time?)

Related

How to update a CGFrame?

The following code has a (at least one) deep flaw: Once CGFrame is set it's location is fixed.
-(UIImageView*) ownImageView {
if (ownImageView == nil) {
ownImageView = [[UIImageView alloc] initWithFrame:
CGRectMake(self.xPosition, self.yPosition,
[constants cardWidth], [constants cardHeight])];
[ownImageView setImage:self.faceImage];
}
return ownImageView;
}
I would like to reuse the UIImageView (once created, I don't want to realloc it). Instead, i'd like to change x,y coordinates of the frame it contains.
The following code works fine, however, it seems carelessly wasteful
-(UIImageView*) ownImageView {
[ownImageView removeFromSuperview];
ownImageView = [[UIImageView alloc] initWithFrame:
CGRectMake(self.xPosition, self.yPosition,
[constants cardWidth], [constants cardHeight])];
[ownImageView setImage:self.faceImage];
return ownImageView;
}
Question: How would you handle this, while keeping as much of lazy instantiation as possible?
The view represents a 2d rectangle moving along the screen. I'd like to represent the "move" by simply changing X/Y position of the existing view instead of replacing it with a new copy.
ownImageView.frame = CGRectMake(x,y,w,h);
If you want to move it the simplest thing to do is to let Core Animation do the work for you.
For more details and options check UIView Class Reference
[UIView animateWithDuration:2
delay:0
options:UIViewAnimationCurveEaseInOut
animations:^{
self.THE_VIEW.center = CGPointMake(200, 200);
// OR you can change it's frame if you prefer
}
completion:NULL];

NSView CALayer Opacity Madness

It seems as though the layer's properties of my NSView are sometimes not editable/wrong. In the code below, the animation works perfectly, and all appears normal. The output from the NSlogs are always :
anim over opacity = 1.00000
first opacity = 0.50000
current opacity = 0.00000
updated opacity = 0.00000
The first two logs look right, so even at animation did stop, the layer seems to operate normally. However, some time later, when I check the opacity it magically turned to 0. Further wrong, when I set the layer's opacity to 1, and check it immediately after, it still is 0. How is that possible?
I goofed around with setneedsdisplay in the layer and setneedsdisplay:YES in nsview and that
didn't help. Any suggestions are greatly appreciated.
- (void) someSetupAnimationMethod {
aLayer = [CALayer layer];
[theView setWantsLayer:YES];
[theView setLayer:aLayer];
[aLayer setOpacity:0.0];
CABasicAnimation *opacity = [CABasicAnimation animationWithKeyPath:#"opacity"];
opacity.byValue = [NSNumber numberWithFloat:1.0];
opacity.duration = 0.3;
opacity.delegate = self;
opacity.fillMode = kCAFillModeForwards;
opacity.removedOnCompletion = NO;
[opacity setValue:#"opacity done" forKey:#"animation"];
[aLayer addAnimation:opacity forKey:nil];
}
- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
if([[anim valueForKey:#"animation"] isEqualToString:#"opacity done"]) {
NSLog(#"anim over opacity = %f", aLayer.opacity);
aLayer.opacity = 0.5;
[aLyaer removeAllAnimations];
NSLog(#"first opacity = %f", aLayer.opacity);
}
}
- (void) someLaterMethod {
NSLog(#"current opacity = %f", aLayer.opacity);
aLayer.opacity = 1.0;
NSLog(#"updated opacity = %f", aLayer.opacity);
}
You're breaking a fundamental CALayer/NSView rule by creating a layer-backed view and then trying to manipulate the layer directly.
aLayer = [CALayer layer];
[theView setWantsLayer:YES];
[theView setLayer:aLayer];
When you tell the view to use a layer before calling setLayer:, the view becomes "layer-backed" -- it is simply using the layer as a buffer and all drawing that you want to do should be done through the usual drawRect: and related methods. You are not allowed to touch the layer directly. If you change the order of those calls:
[theView setLayer:aLayer];
[theView setWantsLayer:YES];
You now have a "layer-hosting" view, which means that the view just provides a space on the screen for the layer to be drawn into. In this case, you do your drawing directly into the layer. Since the contents of the layer become the contents of the view, you cannot add subviews or use the view's drawing mechanisms.
This is mentioned in the -[NSView setWantsLayer:] documentation; I also found an old cocoa-dev thread that explains it pretty well.
I'm not certain that this will fix your problem, but as far as I can see you're doing everything else correctly, such as setting the fill mode and updating the value after the animation finishes.

CALayer - clear internal rendering context in favor of custom drawing code

When subclassing an CALayer and implementing the drawInContext method, I would assume that any drawing I do within there is all that will show up, but instead if I set (for example) borderWidth/borderColor then CALayer will draw a border on it's own above all my custom drawing code.
This is a CALayer subclass:
#implementation MyCustomCALayer
- (id)init
{
self = [super init];
if(self)
{
[self setNeedsDisplayOnBoundsChange:YES];
}
return self;
}
- (void)drawInContext:(CGContextRef)context
{
CGRect rect = CGContextGetClipBoundingBox(context);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillRect(context, rect);
}
#end
Created in a UIView something like:
- (void)ensureLayer
{
if(myLayer)
return;
myLayer = [[[MyCustomCALayer alloc] init] autorelease];
myLayer.borderColor = [UIColor greenColor].CGColor;
myLayer.borderWidth = 1;
myLayer.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
}
- (void)layoutSublayersOfLayer:(CALayer *)layer
{
[super layoutSublayersOfLayer:layer];
[self ensureLayer];
if(![[layer.sublayers objectAtIndex:0] isEqual:myLayer])
[layer insertSublayer:myLayer atIndex:0];
}
What happens, is the MyCustomCALayer fills a rectangle with red, this is what I would expect to see and nothing else, since i've implemented the drawInContext method, but instead I see a red rectangle with a green border on top, always on top, i've tried just about every combination I can think of to get rid of the green border being drawn and cannot figure it out.
My reasoning is I would like to use the borderWidth and borderColor and other properties of the CALayer instead of creating my own properties, because the code that I need to draw contains a border, a fill, etc... but the rendering I need to do is not a simple shape. So the only way i've found around this is to set the borderWidth to 0 and add my own property to my subclass, like myBorderWidth, which is ridiculous.
This is done with the latest iOS SDK, but i'd imagine it's the same for Mac.
Hope this makes sense, any thoughts?
Thanks!
You’re out of luck; CoreAnimation doesn’t support overriding its implementation of rendering for the basic layer properties. Please do file a bug.

NSTextField like safari address bar

Whats the best way to go about building an address field like the one in safari?
Needs to have editable text, and determinate progress indicator background.
You could just subclass NSTextField and override the -drawRect: method to "fill" the appropriate percentage of the entire width with some color or gradient (or whatever) for the progress. If I'm understanding your question right.
-(void)drawRect:(NSRect)dirtyRect {
CGFloat progress = 0.33;
NSRect progressRect = [self bounds];
progressRect.size.width *= progress;
[[NSColor colorWithCalibratedRed:0.0 green:0.0 blue:1.0 alpha:0.4] set];
NSRectFillUsingOperation(progressRect, NSCompositeSourceOver);
[super drawRect:dirtyRect];
}
Obviously "progress" would come from a property you declare and be updated according to the model. You'd need to make sure setDrawsBackground: is turned off, or the background is set to [NSColor clearColor]; in order for your custom drawing to be seen.
This is a shot from the code above.

Making UITableView with cell-sized images smooth scrolled

EVERYTHING WRITTEN HERE ACTUALLY WORKS RIGHT
EXEPT FOR [UIImage imageNamed:] METHOD USAGE
Implementation
I am using model in witch you have a custom UITableViewCell with one custom UIView set up as Cell's backgroundView.
Custom UIView contains two Cell-sized images (320x80 px), one of which is 100% transparent to half of the view. All elements are set to be Opaque and have 1.0 Alpha property.
I don't reuse Cells because I failed to make them loading different images. Cell's reused one-by-one up to 9 cells overall. So I have 9 reusable Cells in memory.
Cell initWithStyle:reuseIdentifier method part:
CGRect viewFrame = CGRectMake(0.0f, 0.0f, 320.0f, 80.0f);
customCellView = [[CustomCellView alloc] initWithFrame:viewFrame];
customCellView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[self setBackgroundView:customCellView];
CustomCellView's initialization method:
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
self.opaque = YES;
self.backgroundColor = [UIColor UICustomColor];
}
return self;
}
Images are being pre-loaded to NSMutableArray as UIImage objects from PNG files with UIImage's imageNamed: method.
They are being set in UITableViewDelegate's method tableView:cellForRowAtIndexPath: and passed through UITableViewCell with custom method to UIView.
And then drawn in UIView's drawRect: overridden method:
- (void)drawRect:(CGRect)rect {
CGRect contentRect = self.bounds;
if (!self.editing) {
CGFloat boundsX = contentRect.origin.x;
CGFloat boundsY = contentRect.origin.y;
CGPoint point;
point = CGPointMake(boundsX, boundsY);
if (firstImage) { [firstImage drawInRect:contentRect blendMode:kCGBlendModeNormal alpha:1.0f]; }
if (secondImage) { [secondImage drawInRect:contentRect blendMode:kCGBlendModeNormal alpha:1.0f]; }
}
}
As you see images are being drawn with drawInRect:blendMode:alpha: method.
Problem
Well, UITableView can't be scrolled at all, it's being struck on every cell, it's chunky and creepy.
Thoughts
Well digging sample code, stackoverflow and forums gave me thought to use OpenGL ES to pre-render images, but, really, is it that hard to make a smooth scrolling?
What's wrong with just using UIImageViews? Are they not fast enough? (They should be if you're preloading the UIImages).
One thing to note is that [UIImage imageNamed:] won't explicitly load the image data into memory. It'll give you a reference which is backed by the data on disk. You can get around this by making a call to [yourImage CGImage].