Line disappears when drawing with CGContext - objective-c

I am drawing lines in a custom UIView class utilizing drawRect: and CGContext. Everything works fine for the first few lines added but sometimes when I add a new path (ie: CGPoint to CGPoint) to the array of paths to be drawn there is a chance that one of the previously drawn lines will vanish. Why is it vanishing?
- (void)drawRect:(CGRect)rect
{
if ([_paths count]) {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor darkGrayColor].CGColor);
CGContextSetLineWidth(context, 2.0f);
// Drawing code
for (NSDictionary * path in _paths){
CGPoint startPoint = CGPointMake([path[#"startPointx"] floatValue], [path[#"startPointy"] floatValue]);
CGPoint endPoint = CGPointMake([path[#"endPointX"] floatValue], [path[#"endPointY"] floatValue]);
CGContextMoveToPoint(context, startPoint.x, startPoint.y); //start at this point
CGContextAddLineToPoint(context, endPoint.x, endPoint.y); //draw to this point
}
CGContextStrokePath(context);
}
}
EDIT:
NSMutableDictionary * newPath = [[NSMutableDictionary alloc] init];
newPath[#"startPointx"] = [NSString stringWithFormat:#"%f",startPoint.x];
newPath[#"startPointy"] = [NSString stringWithFormat:#"%f",startPoint.y];
newPath[#"endPointX"] = [NSString stringWithFormat:#"%f",endPoint.x];
newPath[#"endPointY"] = [NSString stringWithFormat:#"%f",endPoint.y];
[_paths addObject:newPath];
---Then remove current _customPathView from superview, instantiate new customPathView and add paths as seen below
[_customPathView setPaths:_paths];
[_scrollContentView insertSubview:_customPathView atIndex:0];
[_customPathView setNeedsDisplay];
It should be noted that I am removing the UIView from it's parent view each time a new path is going to be added to the array and then I instantiate a new UIView with the new paths array just to make sure I'm getting a fresh UIView with all the paths being passed.
See in the images that the line gets drawn first from the purple dot to the gray dot, then to the white dot but when the next line gets drawn from the white dot to the next gray dot the previous line vanishes.

Perhaps you need to set clearsContextBeforeDrawing To NO:
self.clearsContextBeforeDrawing=NO;

Related

Drawing outside content insets with drawGlyphsForGlyphRange

I'm trying to show some extra symbols next to lines in NSTextView, based on text attributes.
I have successfully subclassed NSLayoutManager, but it seems that layout manager can't draw outside the area set by textContainerInset.
Because my text view can potentially have a very long strings, I'm hoping to keep the drawing connected to displaying glyphs. Is there a way to trick the layout manager to be able to draw inside the content insets — or is there another method I use instead of drawGlyphsForGlyphRange?
I have tried calling super before and after drawing, as well as storing and not storing graphics state. I also attempted setDrawsOutsideLineFragment:YES for the glyphs, but with no luck.
Things like Xcode editor itself uses change markers, so I know that this is somehow doable, but it's very possible I'm looking from the wrong place.
My drawing method, simplified:
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(NSPoint)origin {
[super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
NSTextStorage *textStorage = self.textStorage;
NSTextContainer *textContainer = self.textContainers[0];
NSRange glyphRange = glyphsToShow;
NSSize offset = self.textContainers.firstObject.textView.textContainerInset;
while (glyphRange.length > 0) {
NSRange charRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL], attributeCharRange, attributeGlyphRange;
id attribute = [textStorage attribute:#"Revision" atIndex:charRange.location longestEffectiveRange:&attributeCharRange inRange:charRange];
attributeGlyphRange = [self glyphRangeForCharacterRange:attributeCharRange actualCharacterRange:NULL];
attributeGlyphRange = NSIntersectionRange(attributeGlyphRange, glyphRange);
if (attribute != nil) {
[NSGraphicsContext saveGraphicsState];
NSRect boundingRect = [self boundingRectForGlyphRange:attributeGlyphRange
inTextContainer:textContainer];
// Find the top of the revision
NSPoint point = NSMakePoint(offset.width - 20, offset.height + boundingRect.origin.y + 1.0);
NSString *marker = #"*";
[marker drawAtPoint:point withAttributes:#{
NSForegroundColorAttributeName: NSColor.blackColor;
}];
[NSGraphicsContext restoreGraphicsState];
}
glyphRange.length = NSMaxRange(glyphRange) - NSMaxRange(attributeGlyphRange);
glyphRange.location = NSMaxRange(attributeGlyphRange);
}
}
The answer was much more simple than I anticipated.
You can set lineFragmentPadding for the associated NSTextContainer to make more room for drawing in the margins. This has to be taken into account when setting insets for the text container.

How could the mere definition of a method in a parent view affect a CAAnimation in a subview?

I have a view that is drawing "marching ants", a dashed line that moves along an NSBezierPath
Looks like this:
CAAnimation* currentLayerAnimation = [self.marchingBlackLayer animationForKey:NEVER_TRANSLATE(#"linePhase")];
if (currentLayerAnimation == nil)
{
float lineWidth = 0.8 / self.previousZoomLevel;
float lineLengthFloat = 5.0 * lineWidth;
NSNumber* lineLength = [[NSNumber alloc] initWithFloat: lineLengthFloat];
self.marchingBlackLayer.lineWidth = lineWidth;
self.marchingBlackLayer.strokeColor = [[NSColor blackColor] CGColor];
self.marchingBlackLayer.fillColor = [[NSColor clearColor] CGColor];
self.marchingBlackLayer.lineDashPattern = #[lineLength, lineLength];
self.marchingWhiteLayer.lineWidth = lineWidth;
self.marchingWhiteLayer.strokeColor = [[NSColor whiteColor] CGColor];
self.marchingWhiteLayer.fillColor = [[NSColor clearColor] CGColor];
self.marchingWhiteLayer.lineDashPattern = #[lineLength, lineLength];
CABasicAnimation* blackMarchingAntsAnimation;
NSNumber* doubleLineLength = [[NSNumber alloc] initWithFloat: (lineLengthFloat * 2)];
blackMarchingAntsAnimation = [CABasicAnimation animationWithKeyPath:NEVER_TRANSLATE(#"lineDashPhase")];
[blackMarchingAntsAnimation setFromValue:#0.0f];
[blackMarchingAntsAnimation setToValue:doubleLineLength];
[blackMarchingAntsAnimation setDuration:0.5f];
[blackMarchingAntsAnimation setRepeatCount:HUGE_VALF];
CABasicAnimation* whiteMarchingAntsAnimation;
NSNumber* tripleLineLength = [[NSNumber alloc] initWithFloat: (lineLengthFloat * 3) ];
whiteMarchingAntsAnimation = [CABasicAnimation animationWithKeyPath:NEVER_TRANSLATE(#"lineDashPhase")];
[whiteMarchingAntsAnimation setFromValue:lineLength];
[whiteMarchingAntsAnimation setToValue:tripleLineLength];
[whiteMarchingAntsAnimation setDuration:0.5f];
[whiteMarchingAntsAnimation setRepeatCount:HUGE_VALF];
[self.marchingBlackLayer addAnimation:blackMarchingAntsAnimation forKey:NEVER_TRANSLATE(#"linePhase")];
[self.marchingWhiteLayer addAnimation:whiteMarchingAntsAnimation forKey:NEVER_TRANSLATE(#"linePhase")];
}
[self.layer addSublayer:self.marchingBlackLayer];
[self.layer addSublayer:self.marchingWhiteLayer];
Everything is dandy. However, the view that is drawing this path has two parent views.
This View's parent is an NSClipView, who's parent is an NSScrollView subclass.
And just by overriding scrollWheel: in the NSScrollView subclass like so:
- (void)scrollWheel:(NSEvent *)theEvent
{
[super scrollWheel:theEvent];
}
The ants animation disappears.
The scrollWheel: method never gets hit, and if I delete that method and run, the ants appear again.
I am completely baffled how this is possible, and am convinced it must have something to do with threading? Unfortunately drawing is my least knowledgable area of Cocoa, so it's difficult to figure out where to start debugging
EDIT:
Throwing in pictures so ya'll can maybe get a better idea of what I'm talking about
Here is before: (take my word that the dashed line is moving around)
Here is after defining the scrollWheel: override (and that's literally the only difference):
EDIT2: Setting a breakpoint on all exceptions doesn't get hit, and the console doesn't output any warnings or anything
EDIT3: Dispatching the animation creation and adding onto the main thread didn't fix it

Cocoa: How to draw inset text as in Mail.app?

How can I draw this style of text in Cocoa (OS X)? It seems to be used in several Apple apps including Mail (as pictured above) and several places in Xcode sidebars. I've looked around but haven't been able to find any resources suggesting how to reproduce this specific style of text. It looks like an inset shadow and my first guess was to try using an NSShadow with the blur radius set to a negative value but apparently only positive values are allowed. Any ideas?
I have some code that draws an embossed cell (originally written Jonathon Mah, I believe). It might not do exactly what you want but it'll give you a place to start:
#implementation DMEmbossedTextFieldCell
#pragma mark NSCell
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView;
{
/* This method copies the three-layer method used by Safari's error page. That's accessible by forcing an
* error (e.g. visiting <foo://>) and opening the web inspector. */
// I tried to use NSBaselineOffsetAttributeName instead of shifting the frame, but that didn't seem to work
const NSRect onePixelUpFrame = NSOffsetRect(cellFrame, 0.0, [NSGraphicsContext currentContext].isFlipped ? -1.0 : 1.0);
const NSRange fullRange = NSMakeRange(0, self.attributedStringValue.length);
NSMutableAttributedString *scratchString = [self.attributedStringValue mutableCopy];
BOOL overDark = (self.backgroundStyle == NSBackgroundStyleDark);
CGFloat (^whenLight)(CGFloat) = ^(CGFloat b) { return overDark ? 1.0 - b : b; };
// Layer 1
[scratchString addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithCalibratedWhite:whenLight(1.0) alpha:1.0] range:fullRange];
[scratchString drawInRect:cellFrame];
// Layer 2
BOOL useGradient = NO; // Safari 5.2 preview has switched to a lighter, solid color look for the detail text. Since we use the same class, use bold-ness to decide
if (self.attributedStringValue.length > 0) {
NSFont *font = [self.attributedStringValue attribute:NSFontAttributeName atIndex:0 effectiveRange:NULL];
if ([[NSFontManager sharedFontManager] traitsOfFont:font] & NSBoldFontMask)
useGradient = YES;
}
NSColor *solidShade = [NSColor colorWithCalibratedHue:200/360.0 saturation:0.03 brightness:whenLight(0.41) alpha:1.0];
[scratchString addAttribute:NSForegroundColorAttributeName value:solidShade range:fullRange];
[scratchString drawInRect:onePixelUpFrame];
// Layer 3 (Safari uses alpha of 0.25)
[scratchString addAttribute:NSForegroundColorAttributeName value:[NSColor colorWithCalibratedWhite:whenLight(1.0) alpha:0.25] range:fullRange];
[scratchString drawInRect:cellFrame];
}
#end
Please don't pick this as the answer I just implemented the above suggestions for fun and put it here because it will probably be useful to someone in the future!
https://github.com/danieljfarrell/InnerShadowTextFieldCell
Following from the advice of indragie and wil-shipley here is a subclass of NSTextFieldCell that draws the text with an inner shadow.
The header file,
// InnerShadowTextFieldCell.h
#import <Cocoa/Cocoa.h>
#interface InnerShadowTextFieldCell : NSTextFieldCell
#property (strong) NSShadow *innerShadow;
#end
Now the implementation file,
// InnerShadowTextFieldCell.m
#import "InnerShadowTextFieldCell.h"
// This class needs the NSString category -bezierWithFont: from,
// https://developer.apple.com/library/mac/samplecode/SpeedometerView/Listings/SpeedyCategories_m.html
#implementation InnerShadowTextFieldCell
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
//// Shadow Declarations
if (_innerShadow == nil) {
/* Inner shadow has not been set, override here with default shadow.
You may or may not want this behaviour. */
_innerShadow = [[NSShadow alloc] init];
[_innerShadow setShadowColor: [NSColor darkGrayColor]];
[_innerShadow setShadowOffset: NSMakeSize(0.1, 0.1)];
/* Trying to find a default shadow radius which will look good for
a label of any size, let's get a rough estimate based on the
hypotenuse of the cell frame. */
[_innerShadow setShadowBlurRadius: 0.0075 * hypot(NSWidth(cellFrame), NSHeight(cellFrame)) ];
}
/* Because we are using the -bezierWithFont: we get a slightly
different path than had we used the superclass to drawn the
text path. This means that the background colour and text
colour looks odd if we use call the superclass's,
-drawInteriorWithFrame:inView: method let's do that
drawing here. Not making the call to super might cause
problems for general use (?) but for a simple label is seems
to work OK */
[self.backgroundColor setFill];
NSRectFill(cellFrame);
NSBezierPath *bezierPath = [self.title bezierWithFont:self.font];
[self.textColor setFill];
[bezierPath fill];
/* The following is inner shadow drawing method is taken from Paint Code */
////// Bezier Inner Shadow
NSShadow *shadow = _innerShadow;
NSRect bezierBorderRect = NSInsetRect([bezierPath bounds], -shadow.shadowBlurRadius, -shadow.shadowBlurRadius);
bezierBorderRect = NSOffsetRect(bezierBorderRect, -shadow.shadowOffset.width, shadow.shadowOffset.height);
bezierBorderRect = NSInsetRect(NSUnionRect(bezierBorderRect, [bezierPath bounds]), -1, -1);
NSBezierPath* bezierNegativePath = [NSBezierPath bezierPathWithRect: bezierBorderRect];
[bezierNegativePath appendBezierPath: bezierPath];
[bezierNegativePath setWindingRule: NSEvenOddWindingRule];
[NSGraphicsContext saveGraphicsState];
{
NSShadow* shadowWithOffset = [shadow copy];
CGFloat xOffset = shadowWithOffset.shadowOffset.width + round(bezierBorderRect.size.width);
CGFloat yOffset = shadowWithOffset.shadowOffset.height;
shadowWithOffset.shadowOffset = NSMakeSize(xOffset + copysign(0.0, xOffset), yOffset + copysign(0.1, yOffset));
[shadowWithOffset set];
[[NSColor grayColor] setFill];
[bezierPath addClip];
NSAffineTransform* transform = [NSAffineTransform transform];
[transform translateXBy: -round(bezierBorderRect.size.width) yBy: 0];
[[transform transformBezierPath: bezierNegativePath] fill];
}
[NSGraphicsContext restoreGraphicsState];
}
#end
This could probably be made more robust but it seems fine for just drawing static labels.
Make sure your change the text color and text background color properties in Interface Builder otherwise you will not be able to see the shadow.
From your screenshot, that looks like text drawn with an inner shadow. Hence, the standard NSShadow method of using a blur radius of 0 won't work because that only draws the shadow under/above the text.
There are two steps to drawing text with an inner shadow.
1. Get the drawing path of the text
To be able to draw a shadow inside the text glyphs, you need to create a bezier path from the string. The Apple sample code SpeedometerView has a category that adds the method -bezierWithFont: to NSString. Run the project to see how this method is used.
2. Fill the path with an inner shadow
Drawing shadows under bezier paths is easy, but drawing a shadow inside one is not trivial. Fortunately, the NSBezierPath+MCAdditions category adds the -[NSBezierPath fillWithInnerShadow:] method to make this easy.

CATextLayer + NSAttributtedString + CTParagraphStyleRef

I want to have some text with a custom line-spacing, so I wrote an attribute string with CTParagraphStyleAttributte and pass it to my CATextLayer:
UIFont *font = [UIFont systemFontOfSize:20];
CTFontRef ctFont = CTFontCreateWithName((CFStringRef)font.fontName,
font.pointSize, NULL);
CGColorRef cgColor = [UIColor whiteColor].CGColor;
CGFloat leading = 25.0;
CTTextAlignment alignment = kCTRightTextAlignment; // just for test purposes
const CTParagraphStyleSetting styleSettings[] = {
{kCTParagraphStyleSpecifierLineSpacingAdjustment, sizeof(CGFloat), &leading},
{kCTParagraphStyleSpecifierAlignment, sizeof(CTTextAlignment), &alignment}
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(styleSettings, 2));
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
(id)ctFont, (id)kCTFontAttributeName,
(id)cgColor, (id)kCTForegroundColorAttributeName,
(id)paragraphStyle, (id)kCTParagraphStyleAttributeName,
nil];
CFRelease(ctFont);
CFRelease(paragraphStyle);
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]
initWithString:string
attributes:attributes];
_textLayer.string = attrStr;
[attrStr release];
But the line height is not changing. I think I am missing something here but I don't know what.
I've tried with kCTParagraphStyleSpecifierLineSpacingAdjustment and kCTParagraphStyleSpecifierLineSpacing but either of them don't seem to work (?). I tried also to set the alignment using kCTParagraphStyleSpecifierAlignment (I know CATextLayer has a property for that) just to test kCTParagraphStyleAttributeName is indeed working and it didn't.
I've noticed that even if I pass some crazy values (for example: CTParagraphStyleCreate(styleSettings, -555);) which leads me to ask myself: Does CATextLayer support paragraph attributes? If so, what am I missing here?
I tried your code, putting the NSAttributedString in a CATextLayer, and it ignored the formatting, as you said.
Then I tried drawing the exact same attributed string to a UIView drawRect method using CTFrameDraw, and it obeyed all your formatting. I can only assume that CATextLayer ignores the majority of its formatting. The CATextLayer Class Reference has a number of warnings about what it does in the interests of efficiency.
If you really need to draw to a CALayer, not a UIView, you may be able to create your own CALayer subclass or delegate and do the drawing there.
- (void)drawRect:(CGRect)rect
{
//
// Build attrStr as before.
//
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect bounds = [self bounds];
// Text ends up drawn inverted, so we have to reverse it.
CGContextSetTextMatrix(ctx, CGAffineTransformIdentity);
CGContextTranslateCTM( ctx, bounds.origin.x, bounds.origin.y+bounds.size.height );
CGContextScaleCTM( ctx, 1, -1 );
// Build a rectangle for drawing in.
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, bounds);
// Create the frame and draw it into the graphics context
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef) attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CFRelease(framesetter);
CFRelease(path);
// Finally do the drawing.
CTFrameDraw(frame, ctx);
CFRelease(frame);
}

Scaling MKMapView Annotations relative to the zoom level

The Problem
I'm trying to create a visual radius circle around a annonation, that remains at a fixed size in real terms. Eg. So If i set the radius to 100m, as you zoom out of the Map view the radius circle gets progressively smaller.
I've been able to achieve the scaling, however the radius rect/circle seems to "Jitter" away from the Pin Placemark as the user manipulates the view.
I'm lead to believe this is much easier to achieve on the forthcoming iPhone OS 4, however my application needs to support 3.0.
The Manifestation
Here is a video of the behaviour.
The Implementation
The annotations are added to the Mapview in the usual fashion, and i've used the delegate method on my UIViewController Subclass (MapViewController) to see when the region changes.
-(void)mapView:(MKMapView *)pMapView regionDidChangeAnimated:(BOOL)animated{
//Get the map view
MKCoordinateRegion region;
CGRect rect;
//Scale the annotations
for( id<MKAnnotation> annotation in [[self mapView] annotations] ){
if( [annotation isKindOfClass: [Location class]] && [annotation conformsToProtocol:#protocol(MKAnnotation)] ){
//Approximately 200 m radius
region.span.latitudeDelta = 0.002f;
region.span.longitudeDelta = 0.002f;
region.center = [annotation coordinate];
rect = [[self mapView] convertRegion:region toRectToView: self.mapView];
if( [[[self mapView] viewForAnnotation: annotation] respondsToSelector:#selector(setRadiusFrame:)] ){
[[[self mapView] viewForAnnotation: annotation] setRadiusFrame:rect];
}
}
}
The Annotation object (LocationAnnotationView)is a subclass of the MKAnnotationView and it's setRadiusFrame looks like this
-(void) setRadiusFrame:(CGRect) rect{
CGPoint centerPoint;
//Invert
centerPoint.x = (rect.size.width/2) * -1;
centerPoint.y = 0 + 55 + ((rect.size.height/2) * -1);
rect.origin = centerPoint;
[self.radiusView setFrame:rect];
}
And finally the radiusView object is a subclass of a UIView, that overrides the drawRect method to draw the translucent circles. setFrame is also over ridden in this UIView subclass, but it only serves to call [UIView setNeedsDisplay] in addition to [UIView setFrame:] to ensure that the view is redrawn after the frame has been updated.
The radiusView object's (CircleView) drawRect method looks like this
-(void) drawRect:(CGRect)rect{
//NSLog(#"[CircleView drawRect]");
[self setBackgroundColor:[UIColor clearColor]];
//Declarations
CGContextRef context;
CGMutablePathRef path;
//Assignments
context = UIGraphicsGetCurrentContext();
path = CGPathCreateMutable();
//Alter the rect so the circle isn't cliped
//Calculate the biggest size circle
if( rect.size.height > rect.size.width ){
rect.size.height = rect.size.width;
}
else if( rect.size.height < rect.size.width ){
rect.size.width = rect.size.height;
}
rect.size.height -= 4;
rect.size.width -= 4;
rect.origin.x += 2;
rect.origin.y += 2;
//Create paths
CGPathAddEllipseInRect(path, NULL, rect );
//Create colors
[[self areaColor] setFill];
CGContextAddPath( context, path);
CGContextFillPath( context );
[[self borderColor] setStroke];
CGContextSetLineWidth( context, 2.0f );
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextAddPath(context, path );
CGContextStrokePath( context );
CGPathRelease( path );
//CGContextRestoreGState( context );
}
Thanks for bearing with me, any help is appreciated.
Jonathan
First, what's foo used in the first function? And I'm assuming radiusView's parent is the annotation view, right?
The "Jittering"
Also, the center point of radiusView should coincide with that of the annotationView. This should solve your problem:
-(void) setRadiusFrame:(CGRect)rect{
rect.origin.x -= 0.5*(self.frame.size.width - rect.size.width);
rect.origin.y -= 0.5*(self.frame.size.height - rect.size.height);
[self.radiusView setFrame:rect]
}
Unnecessary method
You could set the frame directly on the radiusView and avoid the above calculation:
UIView * radiusView = [[[self mapView] viewForAnnotation: annotation] radiusView];
rect = [[self mapView] convertRegion:foo toRectToView: radiusView.superView];
[radiusView setFrame:rect];
When drawing the ellipse, don't use the rect passed to drawRect:, it doesn't have to be the same as the frame. It's safer to directly use self.frame
Unnecessary view
Now I gave the above points if you need to use the above hierarchy, but I don't see why don't you just draw your ellipses directly in the LocationAnnotationView? It's there for this purpose after all. This is how you do this:
when scaling, change the annotationView's rect directly:
rect = [[self mapView] convertRegion:foo toRectToView: self.mapView];
[[[self mapView] viewForAnnotation: annotation] setFrame:rect];
Move the implementation of drawRect: to LocationAnnotationView.
This is easier to implement, and should address your problem as the center point of the annotation view moves with your pin and you shouldn't see this problem.
Fixes
There are two other issues in the code:
Set longitudeDelta like this:
region.span.longitudeDelta = 0.002*cos(region.center.latitude*pi/180.0);
as the longitude delta converted to meters changes with the latitude. Alternatively, you could only set latitude delta, then modify the rect so it becomes rectangular (width==height).
in drawRect:, don't use the passed rect; instead use self.frame. There's no guarantee that these are the same, and rect could have any value.
Let me know if these work ;-)