So this is my first ever attempt at using a CALayer. Build is successful and no reported bugs, so I assume I must be doing something obviously wrong. But the layer does not display at all.
- (void)viewDidLoad
{
// Get Reliant Magenta in amazingly verbose manner
CGColorSpaceRef rgbaColorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat reliantMagentaValues[4] = {(208/255),(27/255),(124/255),0.3f};
CGColorRef reliantMagenta = CGColorCreate(rgbaColorSpace, reliantMagentaValues);
CALayer *reliantCanvasLayer = [CALayer layer];
reliantCanvasLayer.backgroundColor = reliantMagenta;
reliantCanvasLayer.frame = CGRectMake(0, 0, 640, 960);
[super viewDidLoad];
[[[self view] layer] addSublayer:reliantCanvasLayer];
CGColorRelease(reliantMagenta);
}
Instead of a full page of magenta, I get back an empty view of grey. How am I messing up something this simple?
UPDATE
- (void)viewDidLoad
{
[super viewDidLoad];
// Get Reliant Magenta in amazingly verbose manner
CGColorSpaceRef rgbaColorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat reliantMagentaValues[4] = {(208/255),(27/255),(124/255),0.3f};
CGColorRef reliantMagenta = CGColorCreate(rgbaColorSpace, reliantMagentaValues);
[[self view] layer].backgroundColor = reliantMagenta;
CGColorRelease(reliantMagenta);
}
same problem, but view is now black and not displaying elements added in the storyboard
One problem (possibly the only problem) is that you're creating your color with all zero components. When you say 208/255, the compiler performs the division using integers and drops the remainder, so 208/255 is 0. You need to divide in floating point: 208.0f / 255.0f.
It's also much easier to use a UIColor instead of setting up the CGColorSpace and the CGColor yourself. Try this:
- (void)viewDidLoad {
[super viewDidLoad];
UIColor *reliantMagenta = [UIColor colorWithRed:208.0f / 255.0f
green:27.0f / 255.0f blue:124.0f / 255.0f alpha:0.3f];
CALayer *magentaLayer = [CALayer layer];
magentaLayer.frame = CGRectMake(0, 0, 640, 960);
magentaLayer.backgroundColor = reliantMagenta.CGColor;
[self.view.layer addSublayer:magentaLayer];
}
You're adding a layer as a sublayer of itself. [self layer] returns the view's existing layer. If you want to create a separate layer, you have to do it manually. There's probably something in the view system that keeps circular references from iterating out of control.
As a point of reference, you should make it a habit of calling [super viewDidLoad] before you do anything else. And an easier way of creating a CGColor is to create a UIColor and get its CGColor property.
Related
I have a CALayer in OS X that has its magnification and magnification filters setup like below, yet the magnification setting seems to be used regardless of the layer's size.
layer.magnificationFilter = kCAFilterNearest;
layer.minificationFilter = kCAFilterTrilinear;
The trilinear filtering works when the layer is small, but when enlarged, the layer is drawn with what looks like linear + the largest mipmap level, instead of nearest neighbor. As a test, I set the magnification filter to kCAFilterNearest, which causes it to render with kCAFilterNearest for all scale levels - so it seems the magnificationFilter is being used regardless of the size the layer is being drawn.
I tried this same code on iOS and it worked as expected, so this must be some quirk of OS X's rendering.
full code:
#implementation MipView
- (instancetype)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
NSImage *image = [NSImage imageNamed:#"image"];
CALayer *layer = [CALayer layer];
layer.magnificationFilter = kCAFilterNearest;
layer.minificationFilter = kCAFilterTrilinear;
layer.frame = CGRectMake(0, 0, image.size.width, image.size.height);
layer.contents = (__bridge id _Nullable)[image CGImageForProposedRect:NULL context:nil hints:nil];
[self setLayer:[CALayer layer]];
[self setWantsLayer:YES];
[self.layer addSublayer:layer];
return self;
}
- (IBAction)slider:(id)sender {
self.layer.sublayerTransform = CATransform3DMakeScale([sender doubleValue], [sender doubleValue], 1);
}
#end
Is there a way to get Core Animation use the magnification filter value when the layer is scaled up?
This is a bug in OS X. The core animation engineers seem to be looking into it, but haven't fixed it as of maxOS Sierra beta 5
I am developing an app that contains lots of custom NSView objects being moved around. I have implemented a gaussian blur background filter for one of the custom NSView subclasses like so:
- (id)init {
self = [super init];
if (self) {
...
CIFilter *saturationFilter = [CIFilter filterWithName:#"CIColorControls"];
[saturationFilter setDefaults];
[saturationFilter setValue:#.5 forKey:#"inputSaturation"];
CIFilter *blurFilter = [CIFilter filterWithName:#"CIGaussianBlur"];
[blurFilter setDefaults];
[blurFilter setValue:#2.0 forKey:#"inputRadius"];
self.wantsLayer = YES;
self.layer.backgroundColor = [NSColor clearColor].CGColor;
self.layer.masksToBounds = YES;
self.layer.needsDisplayOnBoundsChange = YES;
self.layerUsesCoreImageFilters = YES;
[self updateFrame]; //this is where the frame size is set
self.layer.backgroundFilters = #[saturationFilter, blurFilter];
...
return self;
}
else return nil;
}
This works great and creates a gaussian blur effect within the entire contents of the view. The problem is that I do not want the gaussian blur to cover the entire view. There is about an (intentional) 12px padding between the actual size of the NSView and the drawing of its content box:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSColor* strokeColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:1];
NSColor* fillColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:.2];
...
[strokeColor setStroke];
[fillColor setFill];
NSBezierPath *box = [NSBezierPath bezierPathWithRoundedRect:NSMakeRect(self.bounds.origin.x + 12, self.bounds.origin.y + 12, self.bounds.size.width - 24, self.bounds.size.height - 24) xRadius:6 yRadius:6];
box.lineWidth = 6;
[box stroke];
[box fill];
...
}
The reason for this padding is that there are some pieces of the GUI that inhabit this region and are drawn seamlessly into the containing box. I would like to mask the Blur effect to only have effect on the interior of the drawn box rather than the entire view. Here is what I have tried.
ATTEMPT 1: Create a sublayer
I created a sublayer in the NSView with the appropriately sized frame, and added the blur effect to this sublayer. PROBLEM: The blur effect seems to only apply to the immediate parent layer, so rather than blur the contents behind the NSView, it blurs the contents of the NSView's self.layer (which is basically empty).
ATTEMPT 2: Create a masking layer
I tried to create a masking layer and set it to self.layer.mask. However, since the positions of the GUI content do change (via the DrawRect function), I would need to get a copy of the current layer to use as the masking layer. I tried the following code, but it had no effect.
self.layer.mask = nil;
NSArray *bgFilters = self.layer.backgroundFilters;
self.layer.backgroundFilters = nil;
CALayer *maskingLayer = self.layer.presentationLayer;
self.layer.mask = maskingLayer;
self.layer.backgroundFilters = bgFilters;
ATTEMPT 3: Draw a masking layer directly
I could not find any examples of how to draw directly on a layer. I can not use a static UIImage to mast with, because, as I said above, the mask has to change with user interaction. I was looking for something equivalent to the DrawRect function. Any help would be appreciated.
SO...
It seems to me that the sublayer way would be the best and simplest way to go, if I could just figure out how to change the priority of the blur effect to be the background behind the NSView not the NSView's background layer behind the sublayer.
Well, I would still like to know if there is a more elegant way, but I have found a solution that works. Basically, I have created a masking layer from an NSImage drawn from a modified version of the drawRect function:
- (id)init {
self = [super init];
if (self) {
// SETUP VIEW SAME AS ABOVE
CALayer *maskLayer = [CALayer layer];
maskLayer.contents = [NSImage imageWithSize:self.frame.size flipped:YES drawingHandler:^BOOL(NSRect dstRect) {
[self drawMask:self.bounds];
return YES;
}];
maskLayer.frame = self.bounds;
self.layer.mask = maskLayer;
return self;
}
else return nil;
}
- (void)drawMask:(NSRect)dirtyRect {
[[NSColor clearColor] set];
NSRectFill(self.bounds);
[[NSColor blackColor] set];
// SAME DRAWING CODE AS drawRect
// EXCEPT EVERYTHING IS SOLID BLACK (NO ALPHA TRANSPARENCY)
// AND ONLY NEED TO DRAW PARTS THAT EFFECT THE EXTERNAL BOUNDARIES
}
This code appears to do nothing. Build successful, no errors. No rectangle drawn on screen.
- (void)viewDidLoad
{
[super viewDidLoad];
UIColor *reliantMagenta = [UIColor colorWithRed:208.0f / 255.0f green:27.0f / 255.0f blue:124.0f / 255.0f alpha:1];
CALayer *reliantCanvasLayer = [CALayer layer];
reliantCanvasLayer.frame = CGRectMake(0, 0, 640, 960);
[[[self view] layer] addSublayer:reliantCanvasLayer];
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect leftRect = CGRectMake(0, 0, 200, 300);
CGContextSaveGState(ctx);
CGContextSetFillColorWithColor(ctx, reliantMagenta.CGColor);
CGContextFillRect(ctx, leftRect);
CGContextRestoreGState(ctx);
}
I am just learning Quartz and really thrashing with it. If you want to explain the relationship between UIViews, CALayers, CGLayers, and context that would be a big help too, but not required, just having trouble understanding what is going on.
If you're starting out with Quartz, then you should start with the Quartz 2D Programming Guide, which walks through all of this. Your key mistake here is that there is no context available in viewDidLoad. Drawing of this kind is generally done in drawRect:. Your call to UIGraphicsGetCurrentContext() returns NULL at this point.
After reading through the Programming Guide, you may have more questions, but that's where you should start to learn about custom drawing.
I created an empty Cocoa app on Xcode for OS X, and added:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.view = [[NSView alloc] initWithFrame:NSMakeRect(100, 100, 200, 200)];
self.view.wantsLayer = YES;
self.view.layer = [CALayer layer];
self.view.layer.backgroundColor = [[NSColor yellowColor] CGColor];
self.view.layer.anchorPoint = CGPointMake(0.5, 0.5);
self.view.layer.transform = CATransform3DMakeRotation(30 * M_PI / 180, 1, 1, 1);
[self.window.contentView addSubview:self.view];
}
But the rotated layer's background is clipped by the view's bounding area:
I thought since some version of OS X and iOS, the view won't clip the content of its subviews and will show everything inside and outside? On iOS, I do see that behavior, but I wonder why it shows up like that and how to make everything show? (I am already using the most current Xcode 4.4.1 on Mountain Lion).
(note: if you try the code above, you will need to link to Quartz Core, and possibly import the quartz core header, although I wonder why I didn't import the header and it still compiles perfectly)
It turns out that if the line:
((NSView *)self.window.contentView).wantsLayer = YES;
is added to the very beginning, then it works as expected:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
((NSView *)self.window.contentView).wantsLayer = YES;
self.view = [[NSView alloc] initWithFrame:NSMakeRect(200, 200, 200, 200)];
self.view.wantsLayer = YES;
self.view.layer.backgroundColor = [[NSColor yellowColor] CGColor];
[self.window.contentView addSubview:self.view];
self.view.layer.anchorPoint = CGPointMake(0.5, 0.5);
self.view.layer.transform = CATransform3DMakeRotation(30 * M_PI / 180, 0, 0, 1);
}
So it looks like if all the views are made to be layer backed, then it works the same as it does on iOS. (If there is a quick way to make all views layer backed automatically, that'd be good).
the anchorPoint line cannot be moved before addSubview line, or else it is incorrect, although I wonder why that would make any difference.
The line self.view.layer = [CALayer layer]; can be removed if window.contentView is layer backed. Both the contentView and self.view don't need to set the layer, and I wonder why too.
The transform line cannot be before the addSubview line, or else it won't rotate, and I wonder why too.
The third thing is that, I thought if I go to Interface Builder and make the contentView a class of ContentView (subclassing NSView), and in its init method, do a self.wantsLayer = YES;, then it would work too, but it does not.
But anyway, the code above works, and I will update the reasons above why when I find out more.
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.