NSScrollView messes up NSGradient (corruption) - objective-c

I have a custom box that I've made that is a subclass of NSBox. I override the drawRect: method and draw a gradient in it like this (assuming I already have a start & end color):
-(void)drawRect:(NSRect)dirtyRect {
NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:start endingColor:end];
[gradient drawInRect:dirtyRect angle:270];
[gradient release];
}
Now this box is added as a subview of a prototype view for a NSCollectionView. In the view's original state it looks like this:
And after scrolling the view out of sight and back in again, it looks like this:
Why is my gradient getting corrupted like that, and how can I fix it? Thanks!

That dirtyRect argument doesn’t necessarily represent the entire box. If Cocoa decides that only a subframe of the original frame needs (re)drawing, dirtyRect represents only that subframe. If you’ve drawn a gradient for the entire frame and then (re)draw the same gradient for a subframe, it's likely they won't match.
Try:
[gradient drawInRect:[self bounds] angle:270];
instead.
One further note: it looks like your gradient object could be cached instead of being created/released inside -drawRect:.

Your problem is you're drawing in dirtyFrame, not the entire rectangle of the box. I have no idea if this is correct, but try this:
-(void)drawRect:(NSRect)dirtyRect {
NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:start endingColor:end];
[gradient drawInRect:[self bounds] angle:270];
[gradient release];
}

Related

Objective C - OS X - Issue adding NSShadow to NSImageView

I am trying to add a shadow to a NSImageView on an MAC application.
I created a custom NSImageView class "ShadowView.h" and modified the drawRect: like so:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSShadow *shadow = [[NSShadow alloc] init];
[shadow setShadowBlurRadius:5];
[shadow setShadowOffset:NSMakeSize(30.0, 3.0)];
[shadow setShadowColor:[NSColor redColor]];
[shadow set];
[self setWantsLayer:YES];
[self setShadow:shadow];
}
However nothing happens. Also, when I debug I can see the above code being called. I looked at this question from 5 years ago but it seems to not work anymore
Adding a Shadow to a NSImageView
Thank you!
When adding a shadow to a view, that view's superview also needs to have layer-backing enabled. If it doesn't, the view's shadow gets clipped at its own bounds, as seen in this sample app:
Make sure you call -setWantsLayer:YES on your view's superview (or check the "Core Animation Layer" checkbox in Interface Builder) in order to make sure the shadow is completely visible:
You should set these somewhere else like, initWithFrame: take them out of the drawRect:
[self setWantsLayer:YES];
[self setShadow:shadow];

drawRect: crash by big rect after zomming

My UIView structure:
I have a "master" UIView (actually UIScrollView, but it's only purpose scrolling per pagination).
As a subView on this "master" I have my "pageView" (subclass of UIScrollView). This UIScrollView can have any content (e.g. a UIImageView).
The "master" has another subView: PaintView (subclass of UIView). With this view, I track the finger movements and draw it.
The structure looks like this:
[master.view addSubview: pageView];
[master.view addSubview: paintView];
I know when the user zooms in (pageView is responsible for this) and over delegate/method calls I change the frame of paintView according to the zoom change, during the zoom action.
After zooming (scrollViewDidEndZooming:withView:atScale) I call a custom redraw method of paintView.
Redraw method and drawRect:
-(void)redrawForScale:(float)scale {
for (UIBezierPath *path in _pathArray) {
//TransformMakeScale...
}
[self setNeedsDisplay];
}
-(void)drawRect:(CGRect)rect {
for (UIBezierPath *path in _pathArray) {
[path strokeWithBlendMode:kCGBlendModeNormal alpha:1.0];
}
}
The problem:
When I zoom-in, I receive a memory warning and the app crashes.
In the Allocations profiler I can see that the app own a lot of memory, but I can't see why.
When I don't call 'setNeedDisplay' after my 'redrawForScale' method the app isn't crashing.
When I log the rect in drawRect I see values like this: {{0, 0.299316}, {4688, 6630}}.
The problem is (re)drawing such a huge CGRect (correct me if this is wrong).
The solution for me is to avoid calling a custom drawRect method. In some stackoverflow answers (and I think somewhere in the Appls docs) it was mentioned that a custom drawRect: will reduce the performance (because iOS can't do some magic on a custom drawRect method).
My solution:
Using a CAShapeLayer. For every UIBezierPath I create a CAShapeLayer instance, add the UIBezierPath to the path attribute of the layer and add the layer as a sublayer to my view.
CAShapeLayer *shapeLayer = [[CAShapeLayer alloc] initWithLayer:self.layer];
shapeLayer.lineWidth = 10.0;
shapeLayer.strokeColor = [UIColor blackColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.path = myBezierPath.CGPath;
[self.layer addSublayer:shapeLayer];
With this solution I don't need to implement drawRect. So the App won't crash, even with a big maximumZoomScale.
After changing the UIBezierPath (translate, scale, change color) I need to set the changed bezierPath to the shapeLayer again (set the path attribute of shapeLayer again).

Draw NSShadow inside a NSView

I'm trying to draw a NSShadow on the background of a NSView. I want to use it as a replacement for NSGradient, as I need to support Mac OS X Tiger. How may I do that? I know this must be pretty easy and I must be making some mistake.
Thanks!
The easiest approach may be to just set the shadow properties for the view's layer. If you have a NSView* named view, it'd be something like:
[[view layer] setShadowOpacity:0.5];
Setting the shadow opacity to something greater than 0 will make the shadow visible. The shadow drawn will be similar to the view's alpha channel, so whatever you draw in the view will have a shadow. There are several other shadow attributes that you can set, such as the blur radius. Take a look at the CALayer reference page for more.
If you must use NSShadow, then just set up a shadow before you do your drawing:
- (void)drawRect:(NSRect)rect
{
NSShadow *shadow = [[[NSShadow alloc] init] autorelease];
[shadow setShadowBlurRadius:3.0];
[shadow setShadowOffset:NSMakeSize(0.0, 5.0)];
[shadow setShadowColor:[NSColor colorWithCalibratedWhite:0.0 alpha:0.6]];
[shadow set];
// continue with your drawing...
}

Rounded NSTextFieldCell like iCal

I'm trying to draw an NSTextFieldCell subclass that looks like the rounded event item normal table in iCal.
Based on this question, I've got the following code in my subclass:
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSGradient *gradient = [[NSGradient alloc] initWithStartingColor:[NSColor lightGrayColor] endingColor:[NSColor grayColor]];
[gradient drawInRect:cellFrame angle:90];
controlView.layer.cornerRadius = 0.5f;
[[self title] drawInRect:cellFrame withAttributes:nil];
}
But this just draws the cell as a normal rectangle, with the gradient fill, but without the rounded corners. I'm obviously missing something, but what?
What about calling:
[[textfield cell] setBezelStyle: NSTextFieldRoundedBezel];
Based on this question, I've got the following code in my subclass: …
The accepted answer on that question assumes that the cell is in a text field (i.e., it is the only cell in the view and it effectively is the entire view), and that that view is or can be layer-backed.
That won't work when you're a table column's cell, because you are not supposed to redraw the whole view and making it layer-backed probably isn't going to work correctly. (I'm not sure one can expect layer-backing a text field to work correctly, either. Anything beyond a plain NSView either is made to work layer-backed or isn't; if the documentation doesn't say it is, assume it isn't.)
[gradient drawInRect:cellFrame angle:90];
But this just draws the cell as a normal rectangle, with the gradient fill, but without the rounded corners.
Yup. That's all this method does, so without the rounded corners being already specified (e.g., as the corner radius of a layer), you need to construct and draw the shape with rounded corners yourself.
To do that, create a path for a rectangle with rounded corners, and draw the gradient in that.

How to update a section of an NSView without redrawing the whole view

I have a NSView where I draw thousands of NSBezierPaths. I would like to highlight (change fill color) the a selected one on mousemoved event. At the moment I use in mouseMoved function the following command:
[self setsetNeedsDisplay:YES];
that force a call to drawRect to redraw every path. I would like to redraw only the selected one.
I tried to use addClip in drawRect function:
NSBezierPath * path = ... //builds the path here
[path addClip];
[path fill];
but it seems that drawRect destroys all the other previously drawn paths and redraws only the one clipped.
Is it possible NOT to invalidate all the view when calling drawRect? I mean just to incrementally overwrite what was on the view before?
Thanks,
Luca
You should use [self setNeedsDisplayInRect:…]. Pass the NSRect you want invalidated, and that will be the area passed to the drawRect: call.
Inside drawRect:, check the area that was passed in and only perform the drawing necessary inside of that rectangle.
Also, you might want to look into using NSTrackingArea instead of mouseMoved: – these allow you to set up specific rectangles to trigger updates for.
I think I solved in a faster way, as I do not know a priori which paths are present in a rectangle I want to avoid a loop through all the paths. Fortunately my paths do not change often, so I can cache all the paths in a NSImage. On mouseMoved event I set:
RefreshAfterMouseMoved = YES;
and in drawRect function I put something like:
if (RefreshAfterMouseMoved) {
[cacheImage drawAtPoint:zero fromRect:viewRect operation:1
fraction:(CGFloat)1.0];
//redraw only the hilighted path
}
else{
if (cacheImage) [cacheImage release];
cacheImage = [[NSImage alloc] initWithSize: [self bounds].size ];
[cacheImage lockFocus];
// draw everything here
[cacheImage unlockFocus];
[cacheImage drawAtPoint:zero fromRect:viewRect operation:1
fraction:(CGFloat)1.0];
}
This method can be combined with the above setNeedsDisplayInRect method putting in mousedMoved function:
NSRect a, b, ab;
a = [oldpath bounds];
b = [newpath bounds];
ab = NSUnionRect(a,b);
RefreshAfterMouseMoved = YES;
[self setNeedsDisplayInRect:ab];