I'm trying to draw a PieChart using UIBezierPath, and I'm pretty close to do so, however, I've got a problem, as you can see on the screenshot attached
Here's the code I'm using :
-(void)drawRect:(CGRect)rect
{
CGRect bounds = self.bounds;
CGPoint center = CGPointMake((bounds.size.width/2.0), (bounds.size.height/2.0));
NSManagedObject *gameObject = [SCGameManager sharedInstance].gameObject;
int playerNumber = 0;
int totalOfPlayers = [(NSSet*)[gameObject valueForKey:#"playerColors"] count];
float anglePerPlayer = M_PI*2 / totalOfPlayers;
for (NSManagedObject *aPlayerColor in [gameObject valueForKey:#"playerColors"]){
//Draw the progress
CGFloat startAngle = anglePerPlayer * playerNumber;
CGFloat endAngle = startAngle + anglePerPlayer;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:center radius:self.frame. size.width/2 startAngle:startAngle endAngle:endAngle clockwise:YES];
UIColor *playerColor = [SCConstants getUIColorForPlayer:[[aPlayerColor valueForKey:#"colorIndex"] intValue]];
[playerColor set];
[path fill];
playerNumber++;
}
}
Apparently, I just need to move my Path to the center of the circle, and then close it, but when I'm adding the following line of code :
[path addLineToPoint:self.center];
[path closePath];
It draws something weird :
Do you have any idea of what is going wrong with my code? I'm not a Bezier expert at all, so any help is welcome!
Thanks!
It looks like the center point you're using is the problem. Indeed, if you look at the documentation for the center property of UIView, you'll find:
The center is specified within the coordinate system of its superview and is measured in points.
You want the center point of the view specified in its own coordinate system, not that of its superview. You've already determined the center of the view in its own coordinates here:
CGPoint center = CGPointMake((bounds.size.width/2.0), (bounds.size.height/2.0));
So just change the point you're using as the center point from self.center to center, like this:
[path addLineToPoint:center];
Related
Is there a way to draw a UIView circle with a dotted line border? I want to have control over the spacing between the dots, and the size of the dots. I tried specifying my own pattern image, but when I make it into a circle it doesn't look good:
UIView *mainCircle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
[mainCircle.layer setCornerRadius:100];
[mainCircle.layer setBorderWidth:5.0];
[mainCircle.layer setBorderColor:[[UIColor colorWithPatternImage:[UIImage imageNamed:#"dotted"]] CGColor]];
[self.view addSubview:mainCircle];
[mainCircle setCenter:self.view.center];
Following on from aksh1t's answer and rob's answer, you should use a round line cap, along with a dash pattern to do this.
The only thing I would add is that with the current code, you can end up with results like this:
Notice how at the top, you get an overlap of the dots. This is due to the fact that the circumference of the circle isn't entirely divisible by the number of dots.
You can fix this relatively easily by doing a simple bit of maths before. I wrote a few lines of code that'll allows you to provide an dot diameter value, along with an expected dot spacing - and it will try and approximate the nearest dot spacing that will result in an integral number of dots.
Also, I recommend you take an 100% layered approach, using CAShapeLayer to draw your circle. That way you can easily add animations to it without having to completely re-draw it for each frame.
Something like this should do the trick:
// your dot diameter.
CGFloat dotDiameter = 10.0;
// your 'expected' dot spacing. we'll try to get as closer value to this as possible.
CGFloat expDotSpacing = 20.0;
// the size of your view
CGSize s = self.view.frame.size;
// the radius of your circle, half the width or height (whichever is smaller) with the dot radius subtracted to account for stroking
CGFloat radius = (s.width < s.height) ? s.width*0.5-dotDiameter*0.5 : s.height*0.5-dotDiameter*0.5;
// the circumference of your circle
CGFloat circum = M_PI*radius*2.0;
// the number of dots to draw as given by the circumference divided by the diameter of the dot plus the expected dot spacing.
NSUInteger numberOfDots = round(circum/(dotDiameter+expDotSpacing));
// the calculated dot spacing, as given by the circumference divided by the number of dots, minus the dot diameter.
CGFloat dotSpacing = (circum/numberOfDots)-dotDiameter;
// your shape layer
CAShapeLayer* l = [CAShapeLayer layer];
l.frame = (CGRect){0, 0, s.width, s.height};
// set to the diameter of each dot
l.lineWidth = dotDiameter;
// your stroke color
l.strokeColor = [UIColor blackColor].CGColor;
// the circle path - given the center of the layer as the center and starting at the top of the arc.
UIBezierPath* p = [UIBezierPath bezierPathWithArcCenter:(CGPoint){s.width*0.5, s.height*0.5} radius:radius startAngle:-M_PI*0.5 endAngle:M_PI*1.5 clockwise:YES];
l.path = p.CGPath;
// prevent that layer from filling the area that the path occupies
l.fillColor = [UIColor clearColor].CGColor;
// round shape for your stroke
l.lineCap = kCALineCapRound;
// 0 length for the filled segment (radius calculated from the line width), dot diameter plus the dot spacing for the un-filled section
l.lineDashPattern = #[#(0), #(dotSpacing+dotDiameter)];
[self.view.layer addSublayer:l];
You'll now get the following output:
If you want to use this in a UIView, I would suggest subclassing it and adding the CAShapeLayer as a sublayer. You'll also want to add a masking layer in order to mask the view's contents to inside the border.
I have added an example of this in the full project below.
Full Project: https://github.com/hamishknight/Dotted-Circle-View
The best way to do what you are trying would be to draw a circle UIBezierPath, and set the path to a dotted style. The dotted style path code was taken from this answer.
UIBezierPath * path = [[UIBezierPath alloc] init];
[path addArcWithCenter:center radius:50 startAngle:0 endAngle:2 * M_PI clockwise:YES];
[path setLineWidth:8.0];
CGFloat dashes[] = { path.lineWidth, path.lineWidth * 2 };
[path setLineDash:dashes count:2 phase:0];
[path setLineCapStyle:kCGLineCapRound];
// After you have the path itself, you can either make
// an image and set it in a view or use the path directly
// in the layer of the view you want to.
// This is the code for the image option.
UIGraphicsBeginImageContextWithOptions(CGSizeMake(300, 20), false, 2);
[path stroke];
UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Hiiii
I'm trying to draw in a view an arc with dynamic width.
I have built an ARRAY with several UIBezierPath with arcs then I draw one after the other. this is the code:
- (void)drawRect:(CGRect)rect
{
// Drawing code
CGFloat radious;
if (self.bounds.size.width < self.bounds.size.height) {
radious = self.bounds.size.width / 2;
}
else{
radious = self.bounds.size.height / 2;
}
float oneGradeInRadians = (float)(2 * M_PI) / 360.f;
float radiandToDraw = (float)self.finalAngleRadians - self.initialAngleRadians;
float splitMeasure = oneGradeInRadians;
int numberOfArcs = radiandToDraw / splitMeasure;
NSMutableArray *arrayOfBeziersPaths = [NSMutableArray arrayWithCapacity:numberOfArcs];
for (int i = 0; i < numberOfArcs; i++) {
float startAngle = self.initialAngleRadians + (i * splitMeasure);
float endAngle = self.initialAngleRadians +((i + 1) * splitMeasure);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/2) radius:radious - self.widthLine.floatValue startAngle:startAngle endAngle:endAngle clockwise:YES];
bezierPath.lineWidth = self.widthLine.floatValue + i/20;
float hue = (float)(i / 3.f);
UIColor *color = [UIColor colorWithHue:hue/360.0 saturation:1 brightness:1 alpha:1];
[color setStroke];
[arrayOfBeziersPaths addObject:bezierPath];
}
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineJoin(context, kCGLineJoinMiter);
//We saved the context
CGContextSaveGState(context);
for (int i = 0; i < numberOfArcs; i++) {
[(UIBezierPath *)[arrayOfBeziersPaths objectAtIndex:i] stroke];
[(UIBezierPath *)[arrayOfBeziersPaths objectAtIndex:i] fill];
}
//Restore the contex
CGContextRestoreGState(context);
}
In this way I get the right shape but, also I get an annoying lines between the arcs:
I think the problem may be a property to change between arcs, but I'm totally lost, or maybe there is another better way to build this.
I tried creating several UIBezierPath paths and I added them to an unique UIBezierPath, then I stoke and fill that path. I didn't get the annoying lines, but the problem is I can not modify the line width, so I can not get the same effect.
Any idea? thanks
dont create many such paths. just create two arcs (inner circle, exterior circle) and two straight lines joining the ends. then fill the path.
Add below code in a viewDidLoad of an empty viewController and check.
- (void)viewDidLoad
{
[super viewDidLoad];
CGFloat k =0.5522847498;
UIBezierPath *path =[UIBezierPath bezierPath];
CGFloat radius=130;
[path addArcWithCenter:CGPointMake(0, 0) radius:radius startAngle:M_PI_2 endAngle:M_PI clockwise:NO];
CGFloat start=10;
CGFloat increment=5;
[path addLineToPoint:CGPointMake(-radius-start, 0)];
[path addCurveToPoint:CGPointMake(0, -radius-start-increment) controlPoint1:CGPointMake(-radius-start, -(radius +start)*k) controlPoint2:CGPointMake(-(radius+start)*k, -radius-start-increment)];
[path addCurveToPoint:CGPointMake(radius+start+2*increment, 0) controlPoint1:CGPointMake((radius+start+increment)*k, -radius-start-increment) controlPoint2:CGPointMake(radius+start+2*increment, (-radius-start-increment)*k)];
[path addCurveToPoint:CGPointMake(0, radius+start+3*increment) controlPoint1:CGPointMake(radius+start+2*increment, (radius+start+2*increment)*k) controlPoint2:CGPointMake((radius+start+2*increment)*k,radius+start+3*increment)];
[path addLineToPoint:CGPointMake(0,radius)];
CAShapeLayer *layer =[CAShapeLayer layer];
[layer setFrame:CGRectMake(150, 200, 300, 300)];
[layer setFillColor:[UIColor greenColor].CGColor];
[layer setStrokeColor:[UIColor blackColor].CGColor];
[layer setPath:path.CGPath];
[self.view.layer addSublayer:layer];
}
It produced result like,
The problem here is that you’ve chosen a crazy approach to drawing the shape you’re after. If you take a look at it, you’ll see that it’s actually a filled region bounded on the outside by a circular arc, and on the inner edge by a spiral.
What you should do, therefore, is create a single NSBezierPath, add the outer circular arc to it, then add a line to the start of your spiral, append a spiral (you’ll want to approximate it with Bézier segments) and finally call -closePath. After that, you can -fill the path and you’ll get a good looking result rather than the mess you have above.
I want to draw a dashed line in my sprite kit game, I can use SKShapeNode node to draw a normal line like the following:
UIBezierPath *_path=[UIBezierPath bezierPath];
//1
CGPoint point1 = CGPointMake(100,100);
CGPoint point2 = CGPointMake(150,150);
[_path moveToPoint:point1];
[_path addLineToPoint:point2];
//2
SKShapeNode *line = [SKShapeNode node];
line.path = _path.CGPath;
I tried to set a dashed pattern to UIBezierPath like this:
// adding this code at location 1 or 2 above but no effect
CGFloat dashes[] = {6, 2};
[_path setLineDash:dashes count:2 phase:0];
but the dashed pattern is not applied.
I also tried to create a dashed copy of CGPath directly from UIBezierpath.CGPath property as:
CGFloat dashes[] = {6, 2};
CGPathRef aCGPath= CGPathCreateCopyByDashingPath(_path.CGPath,NULL,0,dashes,2);
line.path = aCGPath;
but also the same.
I really appreciate if someone could explain what is the problem and how can I draw a dashed line between two points by applying dashed cgpath to skshapenode.
Edit: I know for this simple example I could divide the distance between these two points to small fixed distances and moving and drawing dashed line by bezeirpath but consider a free hand path with points came from touches, it is a very complex and inefficient to redraw the path with fixed length points then draw dashes. I wonder if there is a way to apply the dashed pattern to the path and making skshapenode to use it that is my question.
If anyone is still interested in a simple answer to this question:
Use CGPathCreateCopyByDashingPath to create a dashed copy of - [UIBezierCurve CGPath]
CGPathRef CGPathCreateCopyByDashingPath(
CGPathRef path,
const CGAffineTransform *transform,
CGFloat phase,
const CGFloat *lengths,
size_t count
);
and add it to the SKShapeNode's path property.
Example:
// creates a dashed pattern
CGFloat pattern[2];
pattern[0] = 10.0;
pattern[1] = 10.0;
CGPathRef dashed =
CGPathCreateCopyByDashingPath([bezierPath CGPath],
NULL,
0,
pattern,
2);
self.myShapeNode.path = dashed;
CGPathRelease(dashed);
EDIT: For performance, you can add an SKShapeNode to an SKEffectNode and set the shouldRasterize property to YES.
Change the code at location 1 to something like this:
UIBezierPath *_path=[UIBezierPath bezierPath];
CGPoint point1 = CGPointMake(100,100);
CGPoint point2 = CGPointMake(150,150);
CGFloat deltaX = 1;
CGFloat deltaY = 1;
CGPoint tmpPoint = point1;
[_path moveToPoint:point1];
while(tmpPoint.x<point2.x && tmpPoint.y<point2.y){
tmpPoint.x+=deltaX;
tmpPoint.y+=deltaY;
if((tmpPoint.y-point1.y)%2==1){
[_path addLineToPoint:tmpPoint];
}else{
[_path moveToPoint:tmpPoint];
}
}
// If the line is not a 45 degree straight line
// Please modify the while loop accordingly
[_path addLineToPoint:point2];
Being a beginner in iOS drawing I wanted to draw a small tooltip container which consists of a rectangle with an indicator arrow.
I created 2 UIBezierPaths and joined them with "appendPath".
I thought this was the right way to go about this, but after struggling for 1 day, I had to ask for help.
Here's the screenshot of where I am right now:
As you can see the problem is simply that when I try to stroke the joined path, it doesn't gets stroked as a whole, but as separate pieces.
Maybe someone could point me in the right direction to solve this ?
Here's the code:
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// create indicator path
UIBezierPath *indicator = [[UIBezierPath alloc] init];
// corner radius
CGFloat radius = self.cornerRadius;
// main path
UIBezierPath *path;
// format frame
rect = CGRectMake(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height - self.indicatorSize.height);
// assign path
path = [UIBezierPath bezierPathWithRoundedRect: rect
byRoundingCorners:UIRectCornerAllCorners
cornerRadii:CGSizeMake(radius, radius)];
// x point
CGFloat x = (rect.size.width - self.indicatorSize.width) / 2;
[indicator moveToPoint:CGPointMake(x, rect.size.height)];
[indicator addLineToPoint:CGPointMake(x + self.indicatorSize.width / 2, rect.size.height + self.indicatorSize.height)];
[indicator addLineToPoint:CGPointMake(x + self.indicatorSize.width, rect.size.height)];
// close path
[indicator closePath];
// append indicator to main path
[path appendPath:indicator];
[[UIColor redColor] setStroke];
[path setLineWidth:2.0];
[path stroke];
[self.fillColor setFill];
[path fill];
// release indicator
[indicator release];
}
Thanks in advance
You need to draw it all as one path, because at the moment you're telling it you want to draw a whole rectangle, and then a triangle, and that's what it's doing.
From the Apple UIBezierPath documentation:
[appendPath:] adds the commands used to create the path in bezierPath to the end of the receiver’s path. This method does not explicitly try to connect the subpaths in the two objects, although the operations in bezierPath might still cause that effect.
So first it will draw your rectangle, and then it will draw your triangle without attempting to join them
I am trying to implement a custom UIView which is basically a pie menu (something like a cake divided into slices).
For that I am trying to draw a circle and a series of lines from the center, like the rays in a chart's wheel.
I have successfully drawn the circle and and I would now like to draw the lines dividing the circle in slices.
This is what I have so far:
-(void)drawRect:(CGRect)rect{
[[UIColor blackColor] setStroke];
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGFloat minDim = (rect.size.width < rect.size.height) ? rect.size.width : rect.size.height;
CGRect circleRect = CGRectMake(0, rect.size.height/2-minDim/2, minDim, minDim);
CGContextAddEllipseInRect(ctx, circleRect);
CGContextSetFillColor(ctx, CGColorGetComponents([[UIColor yellowColor] CGColor]));
CGContextFillPath(ctx);
CGPoint start = CGPointMake(0, rect.size.height/2);
CGPoint end = CGPointMake(rect.size.width, rect.size.height/2);
for (int i = 0; i < MaxSlices(6); i++){
CGFloat degrees = 1.0*i*(180/MaxSlices(6));
CGAffineTransform rot = CGAffineTransformMakeRotation(degreesToRadians(degrees));
UIBezierPath *path = [self pathFrom:start to:end];
[path applyTransform:rot];
}
}
- (UIBezierPath *) pathFrom:(CGPoint) start to:(CGPoint) end{
UIBezierPath* aPath = [UIBezierPath bezierPath];
aPath.lineWidth = 5;
[aPath moveToPoint:start];
[aPath addLineToPoint:end];
[aPath closePath];
[aPath stroke];
return aPath;
}
The problem is that applyTransform on the path doesn't seem to do anything. The first path gest drawn correctly the and the following ones are unaffected by the rotation. Basically what I see is just one path. Check the screen shot here http://img837.imageshack.us/img837/9757/iossimulatorscreenshotf.png
Thank you for your help!
You're drawing the path (with stroke) before you transform it. A path is just a mathematical representation. It isn't the line "on the screen." You can't move what you've already drawn by modifying the data about it.
Just move the [aPath stroke] out of pathFrom:to: and put it after the applyTransform:.