How to draw a segmented circle in iOS? - ios

I expected to find the answer to this question easily, but, unfortunately, my search produced no results. How to draw such a circle in iOS?

I ended up using UIBezierPath. This is how i draw the given circle:
CGFloat DegreesToRadians(CGFloat degrees)
{
return degrees * M_PI / 180;
};
- (void)drawRect:(CGRect)rect
{
CGFloat radius = 70;
CGPoint center = CGPointMake(90 + radius, 170 + radius);
CGFloat start = DegreesToRadians(-90), end;
NSArray *angles = #[#"0", #"60", #"-90"];
NSArray *colors = #[[UIColor yellowColor], [UIColor blueColor], [UIColor redColor]];
int col_counter = 0;
for (NSString *angle in angles)
{
CGFloat value = [angle floatValue];
end = DegreesToRadians(value);
CGPoint next;
next.x = center.x + radius * cos(start);
next.y = center.y + radius * sin(start);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:center];
[path addLineToPoint:next];
[path addArcWithCenter:center radius:radius startAngle:start endAngle:end clockwise:YES];
[path addLineToPoint:center];
UIColor *color = colors[col_counter];
[color set];
[path fill];
col_counter++;
start = end;
}
}

Using Andrey's answer I was able to add a short piece of code after the loop to add a white circle in the middle to create what looks like a white circle with a thick, multi-coloured border.
end = DegreesToRadians(360);
start = DegreesToRadians(-90);
int width = 10; // thickness of the circumference
radius -= width;
CGPoint next;
next.x = center.x + radius * cos(start);
next.y = center.y + radius * sin(start);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:center];
[path addLineToPoint:next];
[path addArcWithCenter:center radius:radius startAngle:start endAngle:end clockwise:YES];
[path addLineToPoint:center];
UIColor *color = [UIColor whiteColor];
[color set];
[path fill];

Related

Draw path around visible part of UIBezierPath

Is it possible to draw a path around the visible part of a UIBezierPath?
Here's an example of my problem
Here's what I'd like to accomplish
Here's what I got so far:
- (void)drawRect:(CGRect)rect {
CGFloat side = MIN(rect.size.width, rect.size.height);
CGPoint center = CGPointMake(rect.size.width / 2.0f, rect.size.height / 2.0f);
UIColor *yinYangColor = [UIColor whiteColor];
UIBezierPath *yinYangPath = [UIBezierPath bezierPath];
// Draw Yin&Yang part
[yinYangPath addArcWithCenter:CGPointMake(center.x, center.y - side / 4.0f) radius:side / 4.0f startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:YES];
[yinYangPath addArcWithCenter:CGPointMake(center.x, center.y + side / 4.0f) radius:side / 4.0f startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:NO];
[yinYangPath addArcWithCenter:CGPointMake(center.x, center.y) radius:side / 2.0f startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:YES];
[yinYangPath closePath];
[yinYangColor setFill];
[yinYangPath fill];
// Add border
CAShapeLayer *borderLayer = [[CAShapeLayer alloc] init];
borderLayer.path = yinYangPath.CGPath;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.strokeColor = [UIColor blackColor].CGColor;
borderLayer.lineWidth = 5.0f;
[self.layer addSublayer:borderLayer];
}
The angle π/2 radians is along the positive y axis.
In standard UIKit geometry, the positive y axis points down toward the bottom of the screen. Therefore the top arc (at center.y - side/4) needs to start at angle -π/2 and end at angle π/2. Since you got these backward, your second arc doesn't start where your first arc ended, so your path contains a straight line connecting those points. Ditto for your second and third arcs. The single straight line visible in your image is actually the combination of those two lines.
Also, incidentally, the rect passed to drawRect: is in theory not necessarily the bounds of the view. It's better not to treat it as such.
Also also, you shouldn't add sublayers in drawRect:. You should do that in init or layoutSubviews and you should make sure you don't duplicate layers. I guess maybe you're using a CAShapeLayer because you don't want the border cut off. I would solve that by insetting the view bounds by the border width:
- (void)drawRect:(CGRect)dirtyRect {
CGFloat lineWidth = 4;
CGRect rect = CGRectInset(self.bounds, lineWidth / 2, lineWidth / 2);
CGFloat side = MIN(rect.size.width, rect.size.height);
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
UIBezierPath *path = [UIBezierPath bezierPath];
CGFloat smallRadius = side / 4;
[path addArcWithCenter:CGPointMake(center.x, center.y - smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:NO];
[path addArcWithCenter:CGPointMake(center.x, center.y + smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:YES];
[path addArcWithCenter:center radius:side / 2 startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:NO];
[path closePath];
[path setLineJoinStyle:kCGLineJoinRound];
[path setLineWidth:lineWidth];
[[UIColor whiteColor] setFill];
[path fill];
[[UIColor blackColor] setStroke];
[path stroke];
}
Result:
If you want the bottom tip to be more pointy, I would do that by clipping all drawing to the path, then drawing the border twice as thick. Half the border will be drawn outside the path and clipped away, leaving a sharp point. In this case, you don't have to inset the bounds.
- (void)drawRect:(CGRect)dirtyRect {
CGFloat lineWidth = 4 * 2;
CGRect rect = self.bounds;
CGFloat side = MIN(rect.size.width, rect.size.height);
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
UIBezierPath *path = [UIBezierPath bezierPath];
CGFloat smallRadius = side / 4;
[path addArcWithCenter:CGPointMake(center.x, center.y - smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:NO];
[path addArcWithCenter:CGPointMake(center.x, center.y + smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:YES];
[path addArcWithCenter:center radius:side / 2 startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:NO];
[path closePath];
[path setLineJoinStyle:kCGLineJoinRound];
[path addClip];
[path setLineWidth:lineWidth];
[[UIColor whiteColor] setFill];
[path fill];
[[UIColor blackColor] setStroke];
[path stroke];
}
Result:

ObjectiveC UIBezierPath path close issue

I try to hexagon and I have some issue in the close path.
Here is my hexagon, and the close path is not smooth.
Here is my drawing code
CAShapeLayer* shapeLayer = [CAShapeLayer layer];
UIBezierPath* path = [UIBezierPath bezierPath];
// [path setLineJoinStyle:kCGLineJoinRound];
// [path setLineJoinStyle:kCGLineJoinBevel];
[path setLineJoinStyle:kCGLineJoinMiter];
// CGFloat dashes[] = {6, 2};
// [path setLineDash:dashes count:2 phase:0];
// [path stroke];
CGFloat radians = 100.0;
NSInteger num = 6;
CGFloat interval = 2*M_PI/num;
NSInteger initX = radians*cosf(interval);
NSInteger initY = radians*sinf(interval);
[path moveToPoint:CGPointMake(location.x - semiWidth + initX, location.y - semiHeight + initY)];
for(int i=1; i<=num; i++){
CGFloat x = radians*cosf(i*interval);
CGFloat y = radians*sinf(i*interval);
[path addLineToPoint:CGPointMake(location.x - semiWidth + x, location.y - semiHeight + y)];
}
[path closePath];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = [[UIColor yellowColor] CGColor];
shapeLayer.fillColor = [[UIColor brownColor] CGColor];
shapeLayer.lineWidth = 4.0f;
Also I try to use different options as following with no luck
[path setLineJoinStyle:kCGLineJoinRound];
[path setLineJoinStyle:kCGLineJoinBevel];
[path setLineJoinStyle:kCGLineJoinMiter];
The problem is that you're not making your first point (the point you move-to) the same way you're making your other points (the points you line-to).
NSInteger initX = radians*cosf(interval);
NSInteger initY = radians*sinf(interval);
[path moveToPoint:CGPointMake(
location.x - semiWidth + initX, location.y - semiHeight + initY)];
Instead, make the first point completely parallel with the others:
CGFloat x = radians*cosf(0*interval);
CGFloat y = radians*sinf(0*interval);
[path moveToPoint:CGPointMake(
location.x - semiWidth + x, location.y - semiHeight + y)];
That's exactly the same as what you'll do later with i*interval, and to emphasize that parallelism, I've written 0 as 0*interval. Here's what I ended up with:

Draw Triangle iOS

The code below is drawing me a circle, how can I modify the existing code to draw a triangle instead?
_colorDotLayer = [CALayer layer];
CGFloat width = self.bounds.size.width-6;
_colorDotLayer.bounds = CGRectMake(0, 0, width, width);
_colorDotLayer.allowsGroupOpacity = YES;
_colorDotLayer.backgroundColor = self.annotationColor.CGColor;
_colorDotLayer.cornerRadius = width/2;
_colorDotLayer.position = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
While there are shown several Core Graphics solution, I want to add a Core Animation based solution.
- (void)viewDidLoad
{
[super viewDidLoad];
UIBezierPath* trianglePath = [UIBezierPath bezierPath];
[trianglePath moveToPoint:CGPointMake(0, view3.frame.size.height-100)];
[trianglePath addLineToPoint:CGPointMake(view3.frame.size.width/2,100)];
[trianglePath addLineToPoint:CGPointMake(view3.frame.size.width, view2.frame.size.height)];
[trianglePath closePath];
CAShapeLayer *triangleMaskLayer = [CAShapeLayer layer];
[triangleMaskLayer setPath:trianglePath.CGPath];
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,0, size.width, size.height)];
view.backgroundColor = [UIColor colorWithWhite:.75 alpha:1];
view.layer.mask = triangleMaskLayer;
[self.view addSubview:view];
}
code based on my blog post.
#implementation TriangleView {
CAShapeLayer *_colorDotLayer;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (_colorDotLayer == nil) {
_colorDotLayer = [CAShapeLayer layer];
_colorDotLayer.fillColor = self.annotationColor.CGColor;
[self.layer addSublayer:_colorDotLayer];
}
CGRect bounds = self.bounds;
CGFloat radius = (bounds.size.width - 6) / 2;
CGFloat a = radius * sqrt((CGFloat)3.0) / 2;
CGFloat b = radius / 2;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, -radius)];
[path addLineToPoint:CGPointMake(a, b)];
[path addLineToPoint:CGPointMake(-a, b)];
[path closePath];
[path applyTransform:CGAffineTransformMakeTranslation(CGRectGetMidX(bounds), CGRectGetMidY(bounds))];
_colorDotLayer.path = path.CGPath;
}
- (void)awakeFromNib {
[super awakeFromNib];
self.annotationColor = [UIColor redColor];
}
#end
Result:
I think is more easy than this solutions:
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:(CGPoint){20, 0}];
[path addLineToPoint:(CGPoint){40, 40}];
[path addLineToPoint:(CGPoint){0, 40}];
[path addLineToPoint:(CGPoint){20, 0}];
// Create a CAShapeLayer with this triangular path
// Same size as the original imageView
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = self.viewTriangleCallout.bounds;
mask.path = path.CGPath;
This is my white triangle:
You can change points or rotate if you want:
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:(CGPoint){0, 0}];
[path addLineToPoint:(CGPoint){40, 0}];
[path addLineToPoint:(CGPoint){20, 20}];
[path addLineToPoint:(CGPoint){0, 0}];
// Create a CAShapeLayer with this triangular path
// Same size as the original imageView
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = self.viewTriangleCallout.bounds;
mask.path = path.CGPath;
Example code, it is based on this SO Answer which draws stars:
#implementation TriangleView
-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
int sides = 3;
double size = 100.0;
CGPoint center = CGPointMake(160.0, 100.0);
double radius = size / 2.0;
double theta = 2.0 * M_PI / sides;
CGContextMoveToPoint(context, center.x, center.y-radius);
for (NSUInteger k=1; k<sides; k++) {
float x = radius * sin(k * theta);
float y = radius * cos(k * theta);
CGContextAddLineToPoint(context, center.x+x, center.y-y);
}
CGContextClosePath(context);
CGContextFillPath(context); // Choose for a filled triangle
// CGContextSetLineWidth(context, 2); // Choose for a unfilled triangle
// CGContextStrokePath(context); // Choose for a unfilled triangle
}
#end
Use UIBezeierPaths
CGFloat radius = 20;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, (center.x + bottomLeft.x) / 2, (center.y + bottomLeft.y) / 2);
CGPathAddArcToPoint(path, NULL, bottomLeft.x, bottomLeft.y, bottomRight.x, bottomRight.y, radius);
CGPathAddArcToPoint(path, NULL, bottomRight.x, bottomRight.y, center.x, center.y, radius);
CGPathAddArcToPoint(path, NULL, center.x, center.y, bottomLeft.x, bottomLeft.y, radius);
CGPathCloseSubpath(path);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:path];
CGPathRelease(path);

iOS drawing circle Path for percent state

i try to draw a circle for a given percent value (like if i have 25% i just draw a quarter of the circle). At the moment im just able to draw a full circle within my view. Any ideas to my problem?
Code atm:
- (UIBezierPath *)makeCircleAtLocation:(CGPoint)location radius:(CGFloat)radius
{
self.circleCenter = location;
self.circleRadius = radius;
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:self.circleCenter
radius:self.circleRadius
startAngle:0.0
endAngle:M_PI * 2.0
clockwise:YES];
return path;
}
- (void)drawCircleForLocation{
CGPoint location = CGPointZero;
location.x = self.frame.size.width/2;
location.y = self.frame.size.height/2;
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [[self makeCircleAtLocation:location radius:9] CGPath];
shapeLayer.strokeColor = [[UIColor whiteColor] CGColor];
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 1.5;
[self.layer addSublayer:shapeLayer];
}
2 Pi RAD = 360° = full circle.
25% of circle = 2 PI * 25%
- (UIBezierPath *)makeCircleAtLocation:(CGPoint)location radius:(CGFloat)radius percent:(CGFloat)percent
{
self.circleCenter = location; //????
self.circleRadius = radius; //????
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:location
radius:radius
startAngle:0.0
endAngle:((M_PI * 2.0) * percent)
clockwise:YES];
[path addLineToPoint:location];
[path closePath];
return path;
}

Drawing a bezier curve between a set of given points

What is the best way to draw a bezier curve, in iOS application, that passes through a set of given points
A little more generic way to do it can be achieved by, for example, looking at the BEMSimpleLineGraph GitHub Project (see here for more info: bemsimplelinegraph). Here I extracted a method to draw a bezier curve through a given list of points.
The header file (BezierLine.h):
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <CoreGraphics/CoreGraphics.h>
#interface BezierLine : NSObject
/*
Draws a bezier curved line on the given context
with points: Array of CGPoint values
*/
-(void) drawBezierCurveInContext:(CGContextRef)context withPoints:(NSArray*)points lineColor:(UIColor*)color lineWidth:(CGFloat)lineWidth;
#end
The implementation (BezierLine.m):
#import "BezierLine.h"
#implementation BezierLine
-(void) drawBezierCurveInContext:(CGContextRef)context withPoints:(NSArray*)points lineColor:(UIColor*)color lineWidth:(CGFloat)lineWidth {
if (points.count < 2) return;
CGPoint CP1;
CGPoint CP2;
// LINE
UIBezierPath *line = [UIBezierPath bezierPath];
CGPoint p0;
CGPoint p1;
CGPoint p2;
CGPoint p3;
CGFloat tensionBezier1 = 0.3;
CGFloat tensionBezier2 = 0.3;
CGPoint previousPoint1;
CGPoint previousPoint2;
[line moveToPoint:[[points objectAtIndex:0] CGPointValue]];
for (int i = 0; i < points.count - 1; i++) {
p1 = [[points objectAtIndex:i] CGPointValue];
p2 = [[points objectAtIndex:i + 1] CGPointValue];
const CGFloat maxTension = 1.0f / 3.0f;
tensionBezier1 = maxTension;
tensionBezier2 = maxTension;
if (i > 0) { // Exception for first line because there is no previous point
p0 = previousPoint1;
if (p2.y - p1.y == p1.y - p0.y) tensionBezier1 = 0;
} else {
tensionBezier1 = 0;
p0 = p1;
}
if (i < points.count - 2) { // Exception for last line because there is no next point
p3 = [[points objectAtIndex:i + 2] CGPointValue];
if (p3.y - p2.y == p2.y - p1.y) tensionBezier2 = 0;
} else {
p3 = p2;
tensionBezier2 = 0;
}
// The tension should never exceed 0.3
if (tensionBezier1 > maxTension) tensionBezier1 = maxTension;
if (tensionBezier2 > maxTension) tensionBezier2 = maxTension;
// First control point
CP1 = CGPointMake(p1.x + (p2.x - p1.x)/3,
p1.y - (p1.y - p2.y)/3 - (p0.y - p1.y)*tensionBezier1);
// Second control point
CP2 = CGPointMake(p1.x + 2*(p2.x - p1.x)/3,
(p1.y - 2*(p1.y - p2.y)/3) + (p2.y - p3.y)*tensionBezier2);
[line addCurveToPoint:p2 controlPoint1:CP1 controlPoint2:CP2];
previousPoint1 = p1;
previousPoint2 = p2;
}
CGContextSetAllowsAntialiasing(context, YES);
CGContextSetStrokeColorWithColor(context, color.CGColor);
CGContextSetLineWidth(context, lineWidth);
CGContextAddPath(context, line.CGPath);
CGContextDrawPath(context, kCGPathStroke);
}
#end
You can use it by for example creating an image context using UIGraphicsBeginImageContext and retrieving the context with UIGraphicsGetCurrentContext().
Otherwise you may want to change the code and assign the resulting Path to a CALayer and add that to an UIView.
Hope this helps.
I know this might be late, but just for anyone who is looking for the right answer. Instead of using addLineToPoint to draw the straight line. You can use addCurveToPoint to draw the curve. e.g.
[bezierPath moveToPoint:CGPointMake(0, 0)];
[bezierPath addCurveToPoint:CGPointMake(40, 100)
controlPoint1:CGPointMake(20, 0)
controlPoint2:CGPointMake(20, 100)];
[bezierPath addCurveToPoint:CGPointMake(80, 50)
controlPoint1:CGPointMake(60, 100)
controlPoint2:CGPointMake(60, 50)];
// and you may don't want to close the path
// [bezierPath closePath];
It's really up to you to choose the control points of the curve. I just use the x = last_point_x + 20; y = last_point_y for control point one, and x = current_point_x - 20; y = current_point_y;
and you may want to use other value instead of the 20 as you may have different segment width of the curve.
You can easily google some example of how to create bezier curve on the web. I found this short tut as an example.
You can create a close bezier curve for e.g. with the following code snippet:
UIBezierPath* path = [UIBezierPath bezierPath];
[path moveToPoint:pt1];
[path addLineToPoint:pt2];
[path addLineToPoint:pt3];
[path closePath];
I hope it will help as a starting point.
Please try this.
UIImageView *waterLevel = [[UIImageView alloc] initWithFrame:CGRectMake(0,0,200,200)];
UIGraphicsBeginImageContext(waterLevel.frame.size);
[waterLevel.image drawAtPoint:CGPointZero];
//define BezierPath
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
// Set the starting point of the shape.
[bezierPath moveToPoint:CGPointMake(0, 0)];
[bezierPath addLineToPoint:CGPointMake(waterLevel.frame.size.width, 0)];
[bezierPath addLineToPoint:CGPointMake(waterLevel.frame.size.width, waterLevel.frame.size.height)];
[bezierPath addLineToPoint:CGPointMake(0, waterLevel.frame.size.height)];
[bezierPath closePath];
bezierPath.lineWidth = 15;
//set the stoke color
[[UIColor blackColor] setStroke];
//draw the path
[bezierPath stroke];
// Add to the current Graphic context
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextAddPath(context,bezierPath.CGPath);
waterLevel.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[self.view addSubview:waterLevel];
You can be much more efficient by using the CGPointFromString method:
NSArray *pointArray = #[#"{3.0,2.5}",#"{100.0,30.2}", #"{100.0,200.0}", #"{3.0,200.0}"];
// draw the path
UIBezierPath *aPath = [UIBezierPath bezierPath];
for (NSString *pointString in pointArray) {
if ([pointArray indexOfObject:pointString] == 0)
[aPath moveToPoint:CGPointFromString(pointString)];
else
[aPath addLineToPoint:CGPointFromString(pointString)];
}
[aPath closePath];
UIBezierPath *aPath = [UIBezierPath bezierPath];
// Set the starting point of the shape.
[aPath moveToPoint:CGPointMake(100.0, 0.0)];
// Draw the lines.
[aPath addLineToPoint:CGPointMake(200.0, 40.0)];
[aPath addLineToPoint:CGPointMake(160, 140)];
[aPath addLineToPoint:CGPointMake(40.0, 140)];
[aPath addLineToPoint:CGPointMake(0.0, 40.0)];
[aPath closePath];

Resources