UIBezierPaths Not Showing in CALayer drawLayer inContext - ios

I am using the code below to draw an arc into the drawLayer method of a custom CALayer class but nothing is displayed:
(void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
float r = self.bounds.size.width/2;
CGContextClearRect(ctx, self.bounds); // clear layer
CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
//CGContextFillRect(ctx, layer.bounds);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(0, 0) radius:r startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:NO];
CGContextAddPath(ctx, path.CGPath);
CGContextStrokePath(ctx);
}
Note that if I uncomment the CGContextFillRect(ctx, layer.bounds) line, a rectangle is properly rendered.

The problem I can see is that you don't set a stroke color (but a fill color)
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
When you configure a fill color it is used to fill the path. Same for a stroke color.
If you want to only want to either stroke or fill the path then you should use either CGContextStrokePath or CGContextFillPath but if you want to do both then you should use CGContextDrawPath with kCGPathFillStroke as the "drawing mode".

Thanks to both of you. I finally decided to draw the arc outside the drawLayer method using the following code:
- (id) initWithLayer:(id)layer withRadius:(float)radius
{
self = [super initWithLayer:layer];
// ...
self.strokeColor = [UIColor whiteColor].CGColor;
self.lineWidth = 10.0;
return self;
}
- (void) update:(float)progress
{
// ....
UIBezierPath *arcPath = [UIBezierPath bezierPath];
[arcPath addArcWithCenter:CGPointMake(r, r) radius:r - self.lineWidth/2 startAngle:startAngle endAngle:nextAngle clockwise:YES];
self.path = arcPath.CGPath;
}

It looks like all the arc drawing methods for both CGPath and UIBezierPath have a bug on 64 bit devices where they only work if the clockwise parameter is set to YES. I notice that your non-working code shows clockwise:NO and the working code has clockwise:YES.

Related

Can't remove CAShapeLayer from Super Layer

I'm trying to remove my circle that I've created as a CAShapeLayer and drawn with UIBezier path if I can't receive a measurement. In my (void)drawRect method for my UIView class, I create and draw a circle if a measurement is found. However, if there is no measurement I'd like to get rid of the circle. For some reason I can't.
Here is the code that creates the circle in -(void)drawRect
- (void)drawRect:(CGRect)rect {
CAShapeLayer *grayCircle = [CAShapeLayer layer];
CAShapeLayer *progressArc = [CAShapeLayer layer];
DBHelper *dbHelper = [DBHelper getSharedInstance];
if ([dbHelper getPairedDevice]==nil) {
[grayCircle removeFromSuperlayer]; // I want to remove the grayCirle if no measurement is found AKA getPairedDevice = nil
}
....
if (_latestMeasurement) {
...
// Gray outer circle
UIBezierPath *grayCirclePath=[UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
grayCircle.path = [grayCirclePath CGPath];
grayCircle.position = CGPointMake(realBounds.origin.x, realBounds.origin.y);
grayCircle.fillColor = [UIColor clearColor].CGColor;
grayCircle.lineCap=kCALineCapRound;
grayCircle.strokeColor = [UIColor grayColor].CGColor;
grayCircle.lineWidth = 30;
// Progress arc
UIBezierPath *progressPath=[UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius startAngle:startAngle endAngle:patientOutput clockwise:YES];
progressArc.path = [progressPath CGPath];
progressArc.position = CGPointMake(grayCircle.frame.origin.x, grayCircle.frame.origin.y);
progressArc.fillColor = [UIColor clearColor].CGColor;
progressArc.lineCap=kCALineCapRound;
progressArc.lineWidth = 30;
[self.layer addSublayer:grayCircle];
[self.layer addSublayer:progressArc];
}
So here's the issue: The following lines of code aren't removing the gray circle:
if ([dbHelper getPairedDevice]==nil) {
[grayCircle removeFromSuperlayer];
}
I'm wondering why is that the case, and how can I remove it from the view's sublayer?
You are creating a new reference to grayCircle each time you go through drawRect. When you remove it - you're removing the new one, not the one you drew previously.
If you already have it, you need to find it first, then remove it - going through the subViews, or maintain a reference to it at the class level

Animate CAShapeLayer setPath

I am trying to add animation while drawing a bezierPath. There is base circle darkgray (0 to 360) , green path (-90 to 0) and red path (0 to 90).
#import "ProgressBar.h"
#interface ProgressBar ()
{
CAShapeLayer *lightGrayPath;
}
#end
#implementation ProgressBar
-(id)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if(self){
[self initValues];
}
return self;
}
-(id) initWithCoder:(NSCoder *)aDecoder{
self = [super initWithCoder:aDecoder];
if(self){
[self initValues];
}
return self;
}
-(void)awakeFromNib{
[super awakeFromNib];
[self startAnimation];
}
-(void)initValues{
lightGrayPath = [CAShapeLayer layer];
lightGrayPath.fillColor = [UIColor clearColor].CGColor;
lightGrayPath.lineWidth=2.0f;
lightGrayPath.strokeColor =[UIColor colorWithWhite:0 alpha:0.1].CGColor;
[self.layer addSublayer:lightGrayPath];
}
-(void)startAnimation{
CGRect rect=self.bounds;
CGFloat subPathWidth=10.0;
UIBezierPath *bezierPath_lightGray=[UIBezierPath bezierPath];
bezierPath_lightGray.lineWidth=lightGrayPath.lineWidth;
[bezierPath_lightGray addArcWithCenter:CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)) radius:(rect.size.height / 2)- subPathWidth/2.0
startAngle:0 endAngle:2*M_PI clockwise:YES];
UIBezierPath *bezierPath_green=[UIBezierPath bezierPath];
bezierPath_green.lineWidth=subPathWidth;
[[UIColor greenColor] setStroke];
[bezierPath_green addArcWithCenter:CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)) radius:(rect.size.height / 2)- subPathWidth/2.0 startAngle:-M_PI/2.0 endAngle:0 clockwise:YES];
[bezierPath_green stroke];
[bezierPath_lightGray appendPath:bezierPath_green];
UIBezierPath *bezierPath_red=[UIBezierPath bezierPath];
bezierPath_red.lineWidth=subPathWidth;
[[UIColor redColor] setStroke];
[bezierPath_red addArcWithCenter:CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)) radius:(rect.size.height / 2)- subPathWidth/2.0 startAngle:0 endAngle:M_PI_2 clockwise:YES];
[bezierPath_red stroke];
[bezierPath_lightGray appendPath:bezierPath_red];
lightGrayPath.path=bezierPath_lightGray.CGPath;
// CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"path"];
// pathAnimation.duration = 10.0;
// //pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
// pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
// [lightGrayPath addAnimation:pathAnimation forKey:#"path"];
}
#end
I want to show animation of path being drawn. The lightGrayArc should not be animated. The Green arc and the red arc should be.
But, Since I am using only one CAShapeLayer and appending paths to it. When i use animation , it animates the light gray path, no animation for green and red arc.
I would suggest:
Do not animate in drawRect. That's for drawing a single frame.
There are two ways to animate a path:
Don't use CAShapeLayer at all, but instead define a drawRect that performs stroke on one or more UIBezierPath.
To animate, don't use CABasicAnimation, but rather use a CADisplayLink (a type of timer optimized for screen updates) that changes some properties that define where the path starts and ends and then call setNeedsDisplay, which will trigger a call to drawRect that will use those properties to stroke the path for that frame of the animation.
Even easier, don't use drawRect to stroke anything at all. Just add your gray, green, and red arcs as three different CAShapeLayer instances. Let CAShapeLayer do all the rendering of the arcs for you.
When you want to animate the red and green layers, add CABasicAnimation for those respective layers.

Where's my Bezier path?

I am trying to draw a Bezier path, an arc of a given angle, in response to a pan gesture. Here's what I have inside the function called by the pan gesture:
UIGraphicsBeginImageContext(self.frame.size);
UIBezierPath *path = [self createArcPath];
[[UIColor blackColor] setStroke];
[[UIColor redColor] setFill];
CGContextRef *ref = UIGraphicsGetCurrentContext();
path.lineWidth = 5;
[path fill];
[path stroke];
UIGraphicsEndImageContext();
And here is createArcPath:
- (UIBezierPath *)createArcPath
{
UIBezierPath *aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(150, 150)
radius:75
startAngle:0
endAngle:DEGREES_TO_RADIANS(135)
clockwise:YES];
return aPath;
}
I don't get an error, but I just don't get anything on my screen relating to the arc. What am I missing? This is in a method of a UIView subclass.
I also tried the following method in - (id)initWithFrame:
UIColor *color =[UIColor redColor];
UIGraphicsBeginImageContext(self.frame.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, width);
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextSetStrokeColorWithColor(context, color.CGColor);
CGRect myOval = {100, 100, self.radius, self.radius};
CGContextAddEllipseInRect(context, myOval);
UIGraphicsEndImageContext();
I also don't see anything. What am I missing? Can these methods not be called from a UIView?
The UIGraphicsBeginImageContext/UIGraphicsEndImageContext calls create an off-screen context that you can draw into, then extract an image from. Since it's an off-screen context nothing shows on the screen.
What you probably want to do is create a custom subclass of UIView that overrides drawRect. In your code that responds to user gestures, change the data structure(s) that represent your drawing, then call setNeedsDisplay to trigger the system to ask your view to redraw itself by calling your drawRect method.
As an alternative you can set up your custom view to install a CAShapeLayer in the view, then change the path on the layer, then call setNeedsDisplay on the layer.

Create a Round Gradient Progress Bar in iOS with CoreGraphics

I want to make something like this:
I tried to make it with core Graphics like this:
float radius = CGRectGetWidth(rect)/2.0f - self.circleBorderWidth/2.0f;
float angleOffset = 0;
UIBezierPath *aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect))
radius:radius
startAngle:-angleOffset
endAngle:(mCircleSegs + 1)/(float)kCircleSegs*M_PI*2 - angleOffset
clockwise:YES];
CGPathRef shape = CGPathCreateCopyByStrokingPath(aPath.CGPath, NULL, 3, kCGLineCapSquare, kCGLineJoinMiter, 1.0f);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextAddPath(ctx, shape);
CGContextClip(ctx);
AngleGradientLayer *angle = [[AngleGradientLayer alloc] init];
angle.colors = [NSArray arrayWithObjects:
(id)[UIColor colorWithRed:0 green:0 blue:0 alpha:1].CGColor,
(id)[UIColor colorWithRed:1 green:1 blue:1 alpha:1].CGColor,
nil];
CGContextClipToRect(ctx, self.bounds);
[angle drawInContext:ctx];
[angle release];
But I think I am on the wrong track here. It doesn't look like the example. Is CoreGraphics able to draw something like this? how?
The bezier path you use as a clip seems to be just a fraction of a circle, while in the image you show, the path is more complex : 2 fractions of a circle, linked by 2 lines, the whole path having a 'ring' shape.
This approach should work, I used it for a timer with the same kind of look.
Although I didn't used directly AngleGradientLayer, I modified its - (CGImageRef)newImageGradientInRect:(CGRect)rect method to return a UIImage.
But I had to rotate this image by + PI/2, as Pavlov gradient angular gradient starts horizontally.
I use a UIImage, because it's a background that DOESN'T change, so I saved an instance of this UIImage in my layer, and draw it whenever I update the clipping path
- (void)drawInContext:(CGContextRef)ctx
{
UIBezierPath *currentPath = [self timerPath];
// other drawing code for glow (shadow) and white stroke)
CGContextAddPath(ctx, currentPath.CGPath);
// clip !
CGContextClip(ctx);
CGContextDrawImage(ctx, self.bounds, _circularGradientImage.CGImage);
//_circularGradientImage from modified newImageGradientInRect method.
}
Here's what I get :

How to Get the reverse path of a UIBezierPath

self.myPath=[UIBezierPath bezierPathWithArcCenter:center
radius:200
startAngle:0
endAngle:180
clockwise:YES];
(This much I was able to get up running with some web searching).
I have this path. Now I want to fill the reverse of this path, so leaving this portion and filling everything else. How can I finish the coding? I don't have much info on this.
The problem
The area it is showing after using Cemal Answer previously it only showed a circle with red stroke.
Edit
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor whiteColor];
self.punchedOutPath =
[UIBezierPath bezierPathWithOvalInRect:CGRectMake(50, 50, 400, 400)];
self.fillColor = [UIColor redColor];
self.alpha = 0.8;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
[[self fillColor] set];
UIRectFill(rect);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut);
[[self punchedOutPath] fill];
CGContextSetBlendMode(ctx, kCGBlendModeNormal);
}
Use bezierPathByReversingPath. From the docs (iOS 6.0+ only):
Creates and returns a new bezier path object with the reversed contents of the current path.
so to reverse your path, you'd just:
UIBezierPath* aPath = [UIBezierPath bezierPathWithArcCenter:center
radius:200
startAngle:0
endAngle:180
clockwise:YES];
self.myPath = [aPath bezierPathByReversingPath];
Here's an alternative that doesn't require reversing the path at all.
You have a portion of a view you essentially want to "clip out":
Let's say you want the white area to be [UIColor whiteColor] with 75% alpha. Here's how you do it quickly:
You create a new UIView subclass.
This view has two properties:
#property (retain) UIColor *fillColor;
#property (retain) UIBezierPath *punchedOutPath;
You override its -drawRect: method to do this:
- (void)drawRect:(CGRect)rect {
[[self fillColor] set];
UIRectFill(rect);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut);
[[self punchedOutPath] fill];
CGContextSetBlendMode(ctx, kCGBlendModeNormal);
}
There's a caveat here: The fillColor of the view must not include the alpha component. So in your case, you'd want that to just be [UIColor whiteColor]. You then apply the alpha bit yourself by calling [myView setAlpha:0.75].
What's going on here: This is using a blend mode called "Destination Out". Mathematically it's defined as R = D*(1 - Sa), but in layman's terms it means "Destination image wherever destination image is opaque but source image is transparent, and transparent elsewhere."
So it's going to use the destination (i.e., what's already in the context) wherever the new stuff is transparent (i.e. outside of the bezier path), and then where the bezier path would be opaque, that stuff is going to become transparent. However, the destination stuff must already be opaque. If it's not opaque, the blending doesn't do what you want. This is why you have to provide an opaque UIColor and then do any transparency you want with the view directly.
I ran this myself, with these circumstances:
the window has a [UIColor greenColor] background
the fillColor is white
the punchedOutPath is a oval that's inset 10 points from the edges of the view.
the view has an alpha of 0.75
With the code above, I get this:
The interior is pure green, and the outside has the semi-transparent overlay.
Update
If your covering is an image, then you'll need to create a new image. But the principle is the same:
UIImage* ImageByPunchingPathOutOfImage(UIImage *image, UIBezierPath *path) {
UIGraphicsBeginImageContextWithOptions([image size], YES, [image scale]);
[image drawAtPoint:CGPointZero];
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetBlendMode(ctx, kCGBlendModeDestinationOut);
[path fill];
UIImage *final = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return final;
}
You would then take the result of this function and put it into a UIImageView.
You can put this into a single screen app into the view controller: It will make a yellow background view and a blue layer on top of it that has an oval region cut out by a mask.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// create a yellow background
UIView *bg = [[UIView alloc] initWithFrame:self.view.bounds];
bg.backgroundColor = [UIColor yellowColor];
[self.view addSubview:bg];
// create the mask that will be applied to the layer on top of the
// yellow background
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.fillRule = kCAFillRuleEvenOdd;
maskLayer.frame = self.view.frame;
// create the paths that define the mask
UIBezierPath *maskLayerPath = [UIBezierPath bezierPath];
[maskLayerPath appendPath:[UIBezierPath bezierPathWithRect:CGRectInset(self.view.bounds, 20, 20)]];
// here you can play around with paths :)
// [maskLayerPath appendPath:[UIBezierPath bezierPathWithOvalInRect:(CGRect){{80, 80}, {140, 190}}]];
[maskLayerPath appendPath:[UIBezierPath bezierPathWithOvalInRect:(CGRect){{100, 100}, {100, 150}}]];
maskLayer.path = maskLayerPath.CGPath;
// create the layer on top of the yellow background
CALayer *imageLayer = [CALayer layer];
imageLayer.frame = self.view.layer.bounds;
imageLayer.backgroundColor = [[UIColor blueColor] CGColor];
// apply the mask to the layer
imageLayer.mask = maskLayer;
[self.view.layer addSublayer:imageLayer];
}
this might answer this question as well: UIBezierPath Subtract Path
I have two solution for you.
Draw this path on a CALayer. And use that CALayer as a mask layer for you actual CALayer.
Draw a rectangle with the sizes of you frame before adding arc.
UIBezierPath *path = [UIBezierPath bezierPathWithRect:view.frame];
[path addArcWithCenter:center
radius:200
startAngle:0
endAngle:2*M_PI
clockwise:YES];
I would use second solution. :)

Resources