NSView with masked CIFilter for OS X app - objective-c

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
}

Related

CALayer's magnificationFilter being ignored on OS X

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 get wrong custom background color and black color instead of background image in NSView

I am trying to make trivial app, in which I have a sidebar, and I am trying either to get background color based on RGB values from the PSD file, either to use background image as pattern....
I have make attempts in both ways, and nothing work's so far. Any help will be deeply appreciated.
-(void) drawRect:(NSRect)dirtyRect {
CALayer *viewLayer = [CALayer layer];
[viewLayer setBackgroundColor:CGColorCreateGenericRGB(85.0, 179.0, 217.0, 1.0)]; //RGB plus Alpha Channel
[self setWantsLayer:YES]; // view's backing store is using a Core Animation Layer
[self setLayer:viewLayer];
}
This code should show bluish color, result is almost white color...not even close to what I want.
Second code, show black background, even that my png file is in the folder supporting files.
- (void)drawRect:(NSRect)dirtyRect {
NSGraphicsContext* theContext = [NSGraphicsContext currentContext];
[theContext saveGraphicsState];
[[NSGraphicsContext currentContext] setPatternPhase:NSMakePoint(0,[self frame].size.height)];
[self.customBackgroundColour set];
NSRectFill([self bounds]);
[theContext restoreGraphicsState];
}
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.customBackgroundColour = [NSColor colorWithPatternImage:
[NSImage imageNamed:#"buttonBg.png"]];
}
return self;
}
Again, any help will be deeply appreciated.
If I recall correctly CGColorCreateGenericRGB expects a range from 0.0-1.0 and would explain why it is white. This should fix the white issue.
[viewLayer setBackgroundColor:CGColorCreateGenericRGB(85.0/255.0, 179.0/255.0, 217.0/255.0, 1.0)]; //RGB plus Alpha Channel
Hopefully that helps.

Drawing issues after subclassing NSCollectionView

OK, here's what I have done:
I have an NSCollectionView
I wanted to be able to enable "selecting" items, and drawing a custom border when an items is selected
I subclassed NSCollectionViewItem (to enable selection)
I subclassed NSView for the NSCollectionViewItem view, in order to draw the border
The code
The view item
#implementation MSLibraryCollectionViewItem
- (void)setSelected:(BOOL)flag
{
[super setSelected:flag];
[(MSLibraryCollectionViewView*)[self view] setSelected:flag];
[(MSLibraryCollectionViewView*)[self view] setNeedsDisplay:YES];
}
The custom view
#implementation MSLibraryCollectionViewView
/***************************************
Initialisation
***************************************/
- (MSLibraryCollectionViewView*)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
/***************************************
Drawing
***************************************/
- (void)drawRect:(NSRect)rect
{
if ([self selected]) {
//[[NSColor redColor] setFill];
//NSRectFill(rect);
//[super drawRect:rect];
NSColor* gS = [NSColor colorWithCalibratedRed:0.06 green:0.45 blue:0.86 alpha:1.0];
NSColor* gE = [NSColor colorWithCalibratedRed:0.12 green:0.64 blue:0.94 alpha:1.0];
NSGradient* g = [[NSGradient alloc] initWithStartingColor:gE endingColor:gS];
NSColor *borderColor = [NSColor colorFromGradient:g];
NSRect frameRect = [self bounds];
if(rect.size.height < frameRect.size.height)
return;
NSRect newRect = NSMakeRect(rect.origin.x+5, rect.origin.y+5, rect.size.width-10, rect.size.height-10);
NSBezierPath *textViewSurround = [NSBezierPath bezierPathWithRoundedRect:newRect xRadius:7 yRadius:7];
[textViewSurround setLineWidth:2.0];
[borderColor set];
[textViewSurround stroke];
}
}
However, the seems to be something wrong with drawing. For example:
When resizing the Collection View's container, a weird line appears at the outer box
When an Collection View item is not 100% visible (e.g. because it's been scrolled down), the selection border doesn't appear at all (while I would expect it to draw just the visible portion).
Some Examples
What's going on?
P.S. I'm not a guru with drawing and custom views in Cocoa - so any ideas/help is more than welcome!
You switched from asking about a collection view to talking about an outline view, but I assume that was just a mental hiccup.
When an Outline View item is not 100% visible (e.g. because it's been scrolled down), the selection border doesn't appear at all
(while I would expect it to draw just the visible portion).
That's because of this code in your -drawRect:.
if(rect.size.height < frameRect.size.height)
return;
It's specifically avoiding drawing a partial selection outline.
Regarding the weird line, I doubt that has to do with your collection item view's custom drawing. Does it stop happening if you disable the custom drawing? You could experiment with using an ordinary color rather than using the third-party +colorFromGradient: code you're using.
By the way, this line:
NSRect newRect = NSMakeRect(rect.origin.x+5, rect.origin.y+5, rect.size.width-10, rect.size.height-10);
could be written more simply as:
NSRect newRect = NSInsetRect(rect, 5, 5);

UIButton displaying a triangle

I have a UIButton and i want it to display a triangle. Is there a function to make it a triangle? Since im not using a UIView class im not sure how to make my frame a triangle.
ViewController(m):
- (IBAction)makeTriangle:(id)sender {
UIView *triangle=[[UIView alloc] init];
triangle.frame= CGRectMake(100, 100, 100, 100);
triangle.backgroundColor = [UIColor yellowColor];
[self.view addSubview: triangle];
Do i have to change my layer or add points and connect them to make a triangle with CGRect?
If im being unclear or not specific add a comment. Thank you!
A button is a subclass of UIView, so you can make it any shape you want using a CAShape layer. For the code below, I added a 100 x 100 point button in the storyboard, and changed its class to RDButton.
#interface RDButton ()
#property (strong,nonatomic) UIBezierPath *shape;
#end
#implementation RDButton
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
self.titleEdgeInsets = UIEdgeInsetsMake(30, 0, 0, 0); // move the title down to make it look more centered
self.shape = [UIBezierPath new];
[self.shape moveToPoint:CGPointMake(0,100)];
[self.shape addLineToPoint:CGPointMake(100,100)];
[self.shape addLineToPoint:CGPointMake(50,0)];
[self.shape closePath];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = self.shape.CGPath;
shapeLayer.fillColor = [UIColor yellowColor].CGColor;
shapeLayer.strokeColor = [UIColor blueColor].CGColor;
shapeLayer.lineWidth = 2;
[self.layer addSublayer:shapeLayer];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self.shape containsPoint:[touches.anyObject locationInView:self]])
[super touchesBegan:touches withEvent:event];
}
The touchesBegan:withEvent: override restricts the action of the button to touches within the triangle.
A view's frame is always a rect, which is a rectangle. Even if you apply a transform to it so it no longer looks like a rectangle, the view.frame property will still be a rectangle -- just the smallest possible rectangle that contains the new shape you have produced.
So if you want your UIButton to look like a triangle, the simplest solution is probably to set its type to UIButtonTypeCustom and then to set its image to be a png which shows a triangle and is transparent outside of the triangle.
Then the UIButton itself will actually be rectangle, but will look like a triangle.
If you want to get fancy, you can also customize touch delivery so that touches on the transparent part of the PNG are not recognized (as I believe they would be by default), but that might be a bit trickier.

How to add an animated layer at a specific index

I am adding two CAText layers to a view and animating one of them. I want to animate one layer above the other but it doesn't get positioned correctly in the layer hierarchy until the animation has finished. Can anyone see what I have done wrong? The animation works, it is just running behind 'topcharlayer2' until the animation has finished.
- (CABasicAnimation *)topCharFlap
{
CABasicAnimation *flipAnimation;
flipAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
flipAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(1.57f, 1, 0, 0)];
flipAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(0.0, 1, 0, 0)];
flipAnimation.autoreverses = NO;
flipAnimation.duration = 0.5f;
flipAnimation.repeatCount = 10;
return flipAnimation;
}
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
[self setBackgroundColor:[UIColor clearColor]]; //makes this view transparent other than what is drawn.
[self initChar];
}
return self;
}
static CATransform3D CATransform3DMakePerspective(CGFloat z)
{
CATransform3D t = CATransform3DIdentity;
t.m34 = - 1. / z;
return t;
}
-(void) initChar
{
UIFont *theFont = [UIFont fontWithName:#"AmericanTypewriter" size:FONT_SIZE];
self.layer.sublayerTransform = CATransform3DMakePerspective(-1000.0f);
topHalfCharLayer2 = [CATextLayer layer];
topHalfCharLayer2.bounds = CGRectMake(0.0f, 0.0f, CHARACTERS_WIDTH, 100.0f);
topHalfCharLayer2.string = #"R";
topHalfCharLayer2.font = theFont.fontName;
topHalfCharLayer2.fontSize = FONT_SIZE;
topHalfCharLayer2.backgroundColor = [UIColor blackColor].CGColor;
topHalfCharLayer2.position = CGPointMake(CGRectGetMidX(self.bounds),CGRectGetMidY(self.bounds));
topHalfCharLayer2.wrapped = NO;
topHalfCharLayer1 = [CATextLayer layer];
topHalfCharLayer1.bounds = CGRectMake(0.0f, 0.0f, CHARACTERS_WIDTH, 100.0f);
topHalfCharLayer1.string = #"T";
topHalfCharLayer1.font = theFont.fontName;
topHalfCharLayer1.fontSize = FONT_SIZE;
topHalfCharLayer1.backgroundColor = [UIColor redColor].CGColor;
topHalfCharLayer1.position = CGPointMake(CGRectGetMidX(self.bounds),CGRectGetMidY(self.bounds));
topHalfCharLayer1.wrapped = NO;
//topHalfCharLayer1.zPosition = 100;
[topHalfCharLayer1 setAnchorPoint:CGPointMake(0.5f,1.0f)];
[[self layer] addSublayer:topHalfCharLayer1 ];
[[self layer] insertSublayer:topHalfCharLayer2 atIndex:0];
[topHalfCharLayer1 addAnimation:[self topCharFlap] forKey:#"anythingILikeApparently"];
}
The View which contains this code is loaded by a view controller in loadView. The initChar method is called in the view's initWithFrame method. The target is iOS4. I'm not using setWantsLayer as I've read that UIView in iOS is automatically layer backed and doesn't require this.
A couple thoughts come to mind:
Try adding the 'R' layer to the layer hierarchy before you start the animation.
Instead of inserting the 'T' layer at index 1, use [[self layer] addSublayer: topHalfCharLayer1]; to add it and then do the insert for the 'R' layer with [[self layer] insertSublayer:topHalfCharLayer2 atIndex:0];
Have you tried to play with the layer zPosition? This determines the visual appearance of the layers. It doesn't actually shift the layer order, but will change the way they display--e.g. which layers is in front of/behind which.
I would also suggest you remove the animation code until you get the layer view order sorted. Once you've done that, the animation should just work.
If you have further issues, let me know in the comments.
Best regards.
From the quartz-dev apple mailing list:
Generally in a 2D case, addSublayer will draw the new layer above the
previous. However, I believe this implementation mechanism is
independent of zPosition and probably just uses something like
painter's algorithm. But the moment you add zPositions and 3D, I don't
think you can solely rely on layer ordering. But I am actually unclear
if Apple guarantees anything in the case where you have not set
zPositions on your layers but have a 3D transform matrix set.
So, it seems I have to set the zPosition explicitly when applying 3D transforms to layers.
/* Insert 'layer' at position 'idx' in the receiver's sublayers array.
* If 'layer' already has a superlayer, it will be removed before being
* inserted. */
open func insertSublayer(_ layer: CALayer, at idx: UInt32)