An easy way to draw a circle using CAShapeLayer - ios

In questions like How to draw a smooth circle..., ...Draw Circle... and ...draw filled Circles the question and answer is very broad, contains lots of unnecessary steps and the methods used isn't always the easiest to re-create or manage.
What is an easy way to draw a circle and add it to my UIView?

An easy way to draw a circle is to create a CAShapeLayer and add a UIBezierPath.
objective-c
CAShapeLayer *circleLayer = [CAShapeLayer layer];
[circleLayer setPath:[[UIBezierPath bezierPathWithOvalInRect:CGRectMake(50, 50, 100, 100)] CGPath]];
swift
let circleLayer = CAShapeLayer();
circleLayer.path = UIBezierPath(ovalIn: CGRect(x: 50, y: 50, width: 100, height: 100)).cgPath;
After creating the CAShapeLayer we set its path to be a UIBezierPath.
Our UIBezierPath then draws a bezierPathWithOvalInRect. The CGRect we set will effect its size and position.
Now that we have our circle, we can add it to our UIView as a sublayer.
objective-c
[[self.view layer] addSublayer:circleLayer];
swift
view.layer.addSublayer(circleLayer)
Our circle is now visible in our UIView.
If we wish to customise our circle's color properties we can easily do so by setting the CAShapeLayer's stroke- and fill color.
objective-c
[circleLayer setStrokeColor:[[UIColor redColor] CGColor]];
[circleLayer setFillColor:[[UIColor clearColor] CGColor]];
swift
shapeLayer.strokeColor = UIColor.red.cgColor;
shapeLayer.fillColor = UIColor.clear.cgColor;
Additionall properties can be found over at 's documentation on the subject https://developer.apple.com/.../CAShapeLayer_class/index.html.

Using CAShapeLayer Class makes drawing easy...
1.Create CAShapeLayer Object
2.Create a Circular path
3.Set the path to the wanted CAShapeLayer path
4.Add the layer to your view
let shapeLayer = CAShapeLayer()
let center = view.center
let circulPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: 0, endAngle: 2.0 * CGFloat.pi, clockwise: true)
shapeLayer.path = circulPath.cgPath
view.layer.addSublayer(shapeLayer)
Note That ,here I draw the circle from center of the view.
You can also set the fill color for your circle like bellow:
shapeLayer.fillColor = UIColor.red.cgColor
for further study you can check on
CALayer.com

Related

Change CAShapeLayer stroke position

I have a collectionviewcell cell and I am adding a CAShapeLayer to give my frontImageView a half border. The code I have now successfully creates a half border but not where I want it. Setting stokeEnd=0.5 creates a half border on the bottom of my imageView. I would like the border to be on the right side of the imageView. From the top center to bottom center (right side).
Can I draw the stroke line from the top center of my imageView to the bottom center, along the right side?
CAShapeLayer *shape = [CAShapeLayer layer];
UIBezierPath *path=[UIBezierPath bezierPathWithOvalInRect:cell.frontImageView.frame];
[shape setPath:path.CGPath];
shape.strokeColor=[UIColor purpleColor].CGColor;
shape.fillColor=[UIColor clearColor].CGColor;
shape.lineWidth = 5.0f;
shape.lineCap = kCALineCapRound;
[shape setStrokeEnd:0.5];
[cell.layer addSublayer:shape];
You can create a path by using a general arc path: (Swift 4.2)
UIBezierPath(arcCenter: view.center, radius: view.frame.width / 2, startAngle: -CGFloat.pi / 2.0, endAngle: CGFloat.pi / 2.0, clockwise: true)
Set your shape's path by this path.

iOS UIButton with custom shapes

How to create 2 buttons in iOS with custom shapes. I need to draw two buttons like Diagonal of a rectangle. Left side is one button and right side is another button. I've tried using Bezier paths, But how to make them adaptive for all devices ?
Here is my code that I've tried for one button
UIBezierPath* bezierPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 138, 118)];
[UIColor.blackColor setStroke];
bezierPath.lineWidth = 20;
[bezierPath stroke];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.frame = self.Btn.bounds;
shapeLayer.path = bezierPath.CGPath;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.strokeColor = [UIColor blackColor].CGColor;
shapeLayer.lineWidth = 120;
self.Btn.layer.mask = shapeLayer;
cant u just do one button,
subclass it and hittest the CGPoint to determine which shape you should set highlighted/selected/etc

iOS: How to draw a circle step by step with NSTimer

I'd like to draw a circle without filling (only border of the circle) step by step (like animated timer). 1 spin is equal 1 day (24 hours). I really don't know what to do.
Steps I've made
1) I've tried https://github.com/danielamitay/DACircularProgress (it's too wide line of progress)
2) I've tried to draw a circle with many arcs.
Can you put me some code please. I really confused. Thanks in advance.
EDIT
I'd like to use NSTimer because I have a button which allow user to stop drawning a circle. If user touch a button again - drawning will have to continue.
What I would do is to create a path that is a circle and use that with a CAShapeLayer and animate the strokeEnd similar to what I did in this answer.
It would look something like this (but I didn't run this code so there may be typos and other mistakes):
UIBezierPath *circle = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:0
endAngle:2.0*M_PI
clockwise:YES];
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.bounds = CGRectMake(0, 0, 2.0*radius, 2.0*radius);
circleLayer.path = circle.CGPath;
circleLayer.strokeColor = [UIColor orangeColor].CGColor;
circleLayer.lineWidth = 3.0; // your line width
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 10.0; // your duration
// Animate from no part of the stroke being drawn to the entire stroke being drawn
drawAnimation.fromValue = #0;
drawAnimation.toValue = #1;
Just note that both the path and the shape layer has a position so the circle path should be defined relative to the origin of the shape layers frame. It might be clearer to define the shape layer first and then create an oval inside of its bounds:
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.bounds = CGRectMake(0, 0, 2.0*radius, 2.0*radius);
circleLayer.position = center; // Set center of the circle
// Create a circle inside of the shape layers bounds
UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:circleLayer.bounds];
circleLayer.path = circle.CGPath;
// Same appearance configuration as before
circleLayer.strokeColor = [UIColor orangeColor].CGColor;
circleLayer.lineWidth = 3.0; // your line width
If DACircleProgressenter link description here otherwise works for you, it looks like you can easily set the line thickness.
As opposed to have a simple lineWidth type property, it seems the author of that library sets the thickness based on a ratio to the radius of the circle. This exists as the thicknessRatio property of that class. For example, if your radius is 40, then setting thicknessRatio to 0.025 should yield a line width of 1. That library seems simple and well thought out - consider using it, or learning from it.
The default is set to 0.3, so a circle with a radius of 40 would have a progress line thickness of 12. That's probably what you were seeing.
Good luck!

iOS draw filled Circles

Not a graphics programmer here, so I'm trying to stumble through this. I'm trying to draw 9 filled circles, each a different color, each with a white border. The UIView's frame is CGRectMake (0,0,60,60). See attached image.
The problem is I'm getting "flat spots" on the borders on each side. Following is my code (from the UIView subclass):
- (void)drawRect:(CGRect)rect
{
CGRect borderRect = CGRectMake(0.0, 0.0, 60.0, 60.0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(context, 1.0, 1.0, 1.0, 1.0);
CGContextSetRGBFillColor(context, colorRed, colorGreen, colorBlue, 1.0);
CGContextSetLineWidth(context, 2.0);
CGContextFillEllipseInRect (context, borderRect);
CGContextStrokeEllipseInRect(context, borderRect);
CGContextFillPath(context);
}
If I change to CGRectMake(0,0,56,56) in drawRect, I get flat spots only on the top and left sides, and the bottom & right sides look fine.
Can anyone suggest how I might fix this? It seems to me the border is being clipped by the UIView, but not knowing much about this, I really don't know how to fix it.
Thanks, in advance, for any of you graphics experts' suggestions.
I like the answer from #AaronGolden, just wanted to add:
CGRect borderRect = CGRectInset(rect, 2, 2);
Or, better:
CGFloat lineWidth = 2;
CGRect borderRect = CGRectInset(rect, lineWidth * 0.5, lineWidth * 0.5);
Those circles are just getting clipped to the bounds of the views that draw them. The views must be slightly larger than the circles to be drawn. You can imagine the CGContextStrokeEllipseInRect call tracing a circle of radius 30 and then painting one pixel on each side of the traced curve. Well on the far edges you're going to have one of those pixels just outside the boundary of the view.
Try making your views something like 62x62, or make the circle radius slightly smaller to leave room for the thick stroke in your 60x60 views.
I Wrote this so that you can draw many circles easily.
Add the following code to your .m file:
- (void) circleFilledWithOutline:(UIView*)circleView fillColor:(UIColor*)fillColor outlineColor:(UIColor*)outlinecolor{
CAShapeLayer *circleLayer = [CAShapeLayer layer];
float width = circleView.frame.size.width;
float height = circleView.frame.size.height;
[circleLayer setBounds:CGRectMake(2.0f, 2.0f, width-2.0f, height-2.0f)];
[circleLayer setPosition:CGPointMake(width/2, height/2)];
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(2.0f, 2.0f, width-2.0f, height-2.0f)];
[circleLayer setPath:[path CGPath]];
[circleLayer setFillColor:fillColor.CGColor];
[circleLayer setStrokeColor:outlinecolor.CGColor];
[circleLayer setLineWidth:2.0f];
[[circleView layer] addSublayer:circleLayer];
}
Then Add the following code to your view did load and replace "yourView" with any view that you want to place the circle in. If you want to make a bunch of circles just add some small views to the page and repeat the code below. The circle will become the size of the view you make.
[self circleFilledWithOutline:self.yourView fillColor:[UIColor redColor] outlineColor:[UIColor purpleColor]];
There is a simple way to draw a fill circle
CGContextFillEllipseInRect(context, CGRectMake(x,y,width,height))
Trianna Brannon's answer on Swift 3
func circleFilledWithOutline(circleView: UIView, fillColor: UIColor, outlineColor:UIColor) {
let circleLayer = CAShapeLayer()
let width = Double(circleView.bounds.size.width);
let height = Double(circleView.bounds.size.height);
circleLayer.bounds = CGRect(x: 2.0, y: 2.0, width: width-2.0, height: height-2.0)
circleLayer.position = CGPoint(x: width/2, y: height/2);
let rect = CGRect(x: 2.0, y: 2.0, width: width-2.0, height: height-2.0)
let path = UIBezierPath.init(ovalIn: rect)
circleLayer.path = path.cgPath
circleLayer.fillColor = fillColor.cgColor
circleLayer.strokeColor = outlineColor.cgColor
circleLayer.lineWidth = 2.0
circleView.layer.addSublayer(circleLayer)
}
And invoke func via:
self.circleFilledWithOutline(circleView: myCircleView, fillColor: UIColor.red, outlineColor: UIColor.blue)

Masking a CALayer with another CALayer

I'm trying to make a donut shape with CALayers. One CALayer will be a large circle, the other one will be a smaller circle positioned in its center, masking it.
The large circle displays fine, but whenever I call circle.mask = circleMask; then the view appears empty.
Here's my code:
AriDonut.h
#import <UIKit/UIKit.h>
#interface AriDonut : UIView
-(id)initWithRadius:(float)radius;
#end
AriDonut.m
#import "AriDonut.h"
#import <QuartzCore/QuartzCore.h>
#implementation AriDonut
-(id)initWithRadius:(float)radius{
self = [super initWithFrame:CGRectMake(0, 0, radius, radius)];
if(self){
//LARGE CIRCLE
CALayer *circle = [CALayer layer];
circle.bounds = CGRectMake(0, 0, radius, radius);
circle.backgroundColor = [UIColor redColor].CGColor;
circle.cornerRadius = radius/2;
circle.position = CGPointMake(radius/2, radius/2);
//SMALL CIRLCE
CALayer *circleMask = [CALayer layer];
circleMask.bounds = CGRectMake(0, 0, 10, 10);
circleMask.cornerRadius = radius/2;
circleMask.position = circle.position;
//circle.mask = circleMask;
[self.layer addSublayer:circle];
}
return self;
}
I've tried setting the large circle's superlayer nil like this:
CALayer *theSuper = circle.superlayer;
theSuper = nil;
But it didin't make a difference.
I also tried setting Circle's masksToBounds property to YES and NO, but it didn't make a difference.
Any thoughts?
Indeed, as #David indicates the current (iOS 5.1) CALayer masks can't be reversed, which poses a problem if you want to use them to make a transparent hole a simple circular CALayer.
What you can do to get a donut is make a circular CALayer's backgroundColor transparent, but give it a borderColor and a wide borderWidth. Here's the dunkin' code:
CALayer *theDonut = [CALayer layer];
theDonut.bounds = CGRectMake(0,0, radius, radius);
theDonut.cornerRadius = radius/2;
theDonut.backgroundColor = [UIColor clearColor].CGColor;
theDonut.borderWidth = radius/5;
theDonut.borderColor = [UIColor orangeColor].CGColor;
[self.layer addSublayer:theDonut];
This is pretty easy using UIBezierPath and a CAShapeLayer as a masking layer. Code sample written as though it's in a UIView subclass.
Objective-C:
CGRect outerRect = self.bounds;
CGFloat inset = 0.2 * outerRect.size.width; // adjust as necessary for more or less meaty donuts
CGFloat innerDiameter = outerRect.size.width - 2.0 * inset;
CGRect innerRect = CGRectMake(inset, inset, innerDiameter, innerDiameter);
UIBezierPath *outerCircle = [UIBezierPath bezierPathWithRoundedRect:outerRect cornerRadius:outerRect.size.width * 0.5];
UIBezierPath *innerCircle = [UIBezierPath bezierPathWithRoundedRect:innerRect cornerRadius:innerRect.size.width * 0.5];
[outerCircle appendPath:innerCircle];
CAShapeLayer *maskLayer = [CAShapeLayer new];
maskLayer.fillRule = kCAFillRuleEvenOdd; // Going from the outside of the layer, each time a path is crossed, add one. Each time the count is odd, we are "inside" the path.
maskLayer.path = outerCircle.CGPath;
self.layer.mask = maskLayer;
Swift:
let outerRect = self.bounds
let inset: CGFloat = 0.2 * outerRect.width // adjust as necessary for more or less meaty donuts
let innerDiameter = outerRect.width - 2.0 * inset
let innerRect = CGRect(x: inset, y: inset, width: innerDiameter, height: innerDiameter)
let outerCircle = UIBezierPath(roundedRect: outerRect, cornerRadius: outerRect.width * 0.5)
let innerCircle = UIBezierPath(roundedRect: innerRect, cornerRadius: innerRect.width * 0.5)
outerCircle.appendPath(innerCircle)
let mask = CAShapeLayer()
mask.fillRule = kCAFillRuleEvenOdd
mask.path = outerCircle.CGPath
self.layer.mask = mask
It is the alpha value of the masking layers content that is used as a mask. (If you would add the mask as a sublayer instead of using it as a mask. Everything that is covered by the sublayer would be visible when used as a mask. Everything that is not covered by the sublayer would be hidden when used as a mask.)
Since your small circle is fully transparent , everything is masked away (is hidden). If you set the backgroundColor of it to any, fully opaque color (only the alpha value is used for the mask) then it will let those pixels through.
Note that this is the reverse of what you want. This will leave you with only "the hole of the donut" visible. There is no built in way to do a reverse mask Instead you would have to draw the content of the mask some other way like using a CAShapeLayer or using drawInContext:.
I succeeded with a CAShapeLayer masking a CALayer. To specify the shape of the masking CAShapeLayer I used UIBezierPath.
I posted the code in my answer to this question: How to Get the reverse path of a UIBezierPath. For the donut shape uncomment the commented line.

Resources