UIView Draw Circle with Dotted Line Border - ios

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();

Related

Setting CAShapeLayer lineWidth Less Than 1

In the above screen shot, there are two lines:
Solid line is just a UIView with 1px height
Dashed line is created using this code
- (void)viewDidAppear:(BOOL)animated {
CAShapeLayer *line = [CAShapeLayer layer];
UIBezierPath *linePath=[UIBezierPath bezierPath];
[linePath moveToPoint:CGPointMake(0, 107)];
[linePath addLineToPoint:CGPointMake(self.view.frame.size.width, 107)];
line.lineWidth = 0.5;
line.path=linePath.CGPath;
line.fillColor = [[UIColor blackColor] CGColor];
line.strokeColor = [[UIColor blackColor] CGColor];
[line setLineJoin:kCALineJoinRound];
[line setLineDashPattern: [NSArray arrayWithObjects:[NSNumber numberWithInt:10], [NSNumber numberWithInt:5],nil]];
[[self.view layer] addSublayer:line];
}
Why is a UIView of 1-pixel (1.0) height less than a dashed line of 0.5 height?
I want the dashed line to be as slim as solid line.
When you say the UIView has a 1px height, do you actually mean 1px?
Sizes in UIKit (the lineWidth and frame of the CAShapeLayer and UIView respectively) are measured in points, not pixels. A single point is equivalent to 1 pixel on a 1x display, 2 pixels on a 2x display and 3 pixels on a 3x display.*
Therefore if you want the size of a single pixel in points - you'll want
1.0/[UIScreen mainScreen].scale
The problem that you seem to be having in your image is that you're confusing the frame origin of your UIView with the line of your UIBezierPath. These aren't the same. The frame origin represents the top of your UIView — whereas the line represents the centre of your path.
You will therefore want to offset your line position down by half of its width - which will align it to the centre of the pixel, allowing the stroke to be rendered on a single pixel.**
Something like this should achieve your desired result:
CGFloat pixelWidth = 1.0/[UIScreen mainScreen].scale;
UIView* v = [[UIView alloc] initWithFrame:(CGRect){0, 50, self.view.frame.size.width, pixelWidth}];
v.backgroundColor = [UIColor redColor];
[self.view addSubview:v];
UIBezierPath* p = [UIBezierPath bezierPath];
[p moveToPoint:(CGPoint){0, 50+(pixelWidth*0.5)}];
[p addLineToPoint:(CGPoint){self.view.frame.size.width, 50+(pixelWidth*0.5)}];
CAShapeLayer* s = [CAShapeLayer layer];
s.contentsScale = [UIScreen mainScreen].scale; // ensures the CAShapeLayer renders its contents at the logical scale of the screen
s.frame = self.view.bounds;
s.path = p.CGPath;
s.fillColor = [UIColor clearColor].CGColor;
s.strokeColor = [UIColor greenColor].CGColor;
s.lineWidth = pixelWidth;
s.lineDashPattern = #[#10, #10];
[self.view.layer addSublayer:s];
Generates the following (on an iPhone 6):
*The iPhone 6 Plus behaves a little differently - its physical display's scale (~2.6x) doesn't match the logical scale (3x).
Therefore any drawing you do in it can result in pixel bleeding, as it gets scaled down to be displayed. You can get around this, but it involves delving into Open GL or Metal to do your drawing.
See also here for a nice overview on how each iPhone renders their content.
**On a 2x display, you'll may also need to offset the position of your line by an extra 0.25 points in order to prevent pixel bleeding (as your line will lie on a pixel boundary), as Andrea says.
0.5 point will translate in:
1.5px on #3x retina display
1px on #2x retina display
0.5 on normal display
0.5 is not a physical measure in pixel coordinate system (half pixel doesn't exist), thus the rendering system usually creates an antialiasing around everything that presents a decimal.
This happens also placing UIView's probably you've already seen unwanted blur around them, this happens when the frame has decimal points, usually it can be fixed by passing the frame in the CGRectIntegral function.
To draw one pixel line I quote Apple indications about it:
On a high-resolution display (with a scale factor of 2.0), a line that
is one point wide is not antialiased at all because it occupies two
full pixels (from -0.5 to +0.5). To draw a line that covers only a
single physical pixel, you would need to make it 0.5 points in
thickness and offset its position by 0.25 points

UIBezierPath not drawing arc

I'm trying to draw a very simple circle. Here is my code:
CameraButtonView *buttonView = [[CameraButtonView alloc]initWithFrame:CGRectMake((self.view.frame.size.width / 2) - 45, self.view.frame.size.height * 0.8, 90, 90)];
buttonView.layer.cornerRadius = buttonView.frame.size.width / 2;
buttonView.layer.borderWidth = 2;
buttonView.backgroundColor = [UIColor clearColor];
buttonView.layer.borderColor = [UIColor greenColor].CGColor;
buttonView.clipsToBounds = YES;
[previewView addSubview:buttonView];
then, in drawRect:
UIBezierPath *path1 = [UIBezierPath bezierPath];
[path1 addArcWithCenter:self.center radius:(20) startAngle:0 endAngle:6.28 clockwise:YES];
[path1 setLineWidth:6];
[[UIColor redColor] setStroke];
[path1 stroke];
I have set the center and radius, specified a line width and stroke color, and called stroke. I have set breakpoints in drawRect to make sure the path is not nil, and it is indeed not. Just nothing is drawn.
The only thing I see on screen is a green circle from the code in the view controller where I set the corner radius. I tried calling [buttonView setNeedsDisplay]; as well, and that did not help. I've tried different initializations of the bezier path as well, such as initializing with the arc instead of creating the path and then calling addArcWithCenter.
what am I doing wrong?
Pay attention that a view center is not the center of the view itself, but is the center position of the view in superview coordinates. Probably you are drawing out of screen.
The center is specified within the coordinate system of its superview
and is measured in points. Setting this property changes the values of
the frame properties accordingly.
If you want to know the center of your view do this:
CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds))

How to remove black edge on UIImageView with rounded corners and a border width?

I have the following code to make the UIImageView in each of my UITableView's cells have rounded corners:
- (void)awakeFromNib
{
// Rounded corners.
[[cellImage layer] setCornerRadius:([cellImage frame].size.height / 2)];
[[cellImage layer] setMasksToBounds:YES];
[[cellImage layer] setBorderColor:[[UIColor whiteColor] CGColor]];
[[cellImage layer] setBorderWidth:3]; // Trouble!
}
I want the images to have a bit of a gap between them, and figured I could make use of the border width to make that happen. Below is an image of what actually happened:
It's that faint black border that I want to know how to get rid of. I'd like to think there's a way of doing it using border width. If not, the best approach might be just to resize the image itself and just set the border width to be 0.
Rather than using corner radius, you can create bezier path for the mask, create a shape layer for that path, and then specify that shape layer for the image view's layer's mask:
CGFloat margin = 3.0;
CGRect rect = CGRectInset(imageView.bounds, margin, margin);
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(imageView.bounds.size.width/2, imageView.bounds.size.height/2) radius:radius startAngle:0 endAngle:M_PI*2 clockwise:NO];
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = path.CGPath;
imageView.layer.mask = mask;
What you are doing is, you are clipping the profile image and the border together, which is making the cell's profile image extending through the border.
You are missing this line:-
cellImage.clipsToBounds = YES;

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 Animate Dashed Rectangle Border

I have a UIImageView displaying an image. I want to "highlight" a portion of the image by drawing a rounded rectangle outline. I would like to have the outline drawn with a thick, dashed line that "animates" by continually varying where the "beginning" of the line starts.
I thought about drawing a circle that had the look I want and then simply animating it, but I really need a rectangular solution, so that's out.
Background:
I'm drawing the rounded rectangle border by calculating 8 points and drawing 4 straight lines and 4 curves. (Maybe this can be easier, but it's not the broken part!)
My thinking is that I'll use an "offset" variable that starts at the top-left of the rounded rectangle, where the top-left curve meets the top straight piece. Then, I will increment this "offset" across the top of the rounded rectangle until it reaches the top-right curve, whereupon I will "reset" the "offset" variable to its original value.
This is working pretty much as I'd like, until the "reset" occurs. At this point, the animation is jerky (kind of expected), but it also appears to travel in reverse for a small portion of the time, before resuming "forward" motion. Finally, at the beginning/end of my dashed line, I get an extra long segment on the dashed line. I know they can't all be equal-length (can they? how to calculate?), but how can I make 2 shorter segments rather than 1 longer segment?
Anybody have an idea of what I can do to get a smooth "marching ants" kind of look? Any other ideas on a good way to (using animation) call the user's eye to a particular area of the screen? (It needs to surround a particular area without obscuring it.)
Current code:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, rect);
// Rounded corner will be 10% of average side length (i.e., (w + h) / 2)
float averageSide = ([self HighlightRect].size.width + [self HighlightRect].size.height) / 2.0;
float roundSize = averageSide * 0.10;
// offset is a static, class variable
offset += roundSize / 4.0;
if ([WhereIAmView offset] < roundSize) {
offset = roundSize;
}
if ([WhereIAmView offset] > ([self HighlightRect].size.width - roundSize)) {
offset = roundSize;
}
// Set the "main" color of the rounded rectangle
UIColor *lineColor = [UIColor colorWithRed:027.0/255.0 green:050.0/255.0 blue:224.0/255.0 alpha:1.0];
CGContextSetStrokeColorWithColor(context, [lineColor CGColor]);
CGContextSetLineWidth(context, 16.0);
CGFloat pattern[] = {25.0, 5.0};
CGContextSetLineDash(context, offset, pattern, 2);
CGRect rRect = [self HighlightRect];
// The top left corner
CGPoint topLeft = CGPointMake(rRect.origin.x, rRect.origin.y);
// The top right corner
CGPoint topRight = CGPointMake(rRect.origin.x + rRect.size.width, rRect.origin.y);
// The bottom right corner
CGPoint bottomRight = CGPointMake(rRect.origin.x + rRect.size.width, rRect.origin.y + rRect.size.height);
// The bottom left corner
CGPoint bottomLeft = CGPointMake(rRect.origin.x, rRect.origin.y + rRect.size.height);
// The two points across the top of the rounded rectangle (left to right)
CGPoint point1 = CGPointMake(rRect.origin.x + roundSize, rRect.origin.y);
CGPoint point2 = CGPointMake(rRect.origin.x + rRect.size.width - roundSize, rRect.origin.y);
// The two points along the right of the rounded rectangle (top to bottom)
CGPoint point3 = CGPointMake(rRect.origin.x + rRect.size.width, rRect.origin.y + roundSize);
CGPoint point4 = CGPointMake(rRect.origin.x + rRect.size.width, rRect.origin.y + rRect.size.height - roundSize);
// The two points along the bottom of the rounded rectangle (right to left)
CGPoint point5 = CGPointMake(rRect.origin.x + rRect.size.width - roundSize, rRect.origin.y + rRect.size.height);
CGPoint point6 = CGPointMake(rRect.origin.x + roundSize, rRect.origin.y + rRect.size.height);
// The two points along the left of the rounded rectangle (bottom to top)
CGPoint point7 = CGPointMake(rRect.origin.x, rRect.origin.y + rRect.size.height - roundSize);
CGPoint point8 = CGPointMake(rRect.origin.x, rRect.origin.y + roundSize);
// Move to point 1
CGContextMoveToPoint(context, point1.x, point1.y);
// Add line to point 2 (this is the straight portion across the top)
CGContextAddLineToPoint(context, point2.x, point2.y);
// Add curve to point 3 (this is the rounded portion in top right)
CGContextAddArcToPoint(context, topRight.x, topRight.y, point3.x, point3.y, roundSize);
// Add line to point 4 (this is the straight portion across the right)
CGContextAddLineToPoint(context, point4.x, point4.y);
// Add curve to point 5 (this is the rounded portion in bottom right)
CGContextAddArcToPoint(context, bottomRight.x, bottomRight.y, point5.x, point5.y, roundSize);
// Add line to point 6 (this is the straight portion across the bottom)
CGContextAddLineToPoint(context, point6.x, point6.y);
// Add curve to point 7 (this is the rounded portion in bottom left)
CGContextAddArcToPoint(context, bottomLeft.x, bottomLeft.y, point7.x, point7.y, roundSize);
// Add line to point 8 (this is the straight portion across the left)
CGContextAddLineToPoint(context, point8.x, point8.y);
// Add curve to point 1 (this is the rounded portion in top left)
CGContextAddArcToPoint(context, topLeft.x, topLeft.y, point1.x, point1.y, roundSize);
// Stroke the path
CGContextStrokePath(context);
}
bump
bump bump
Try Using CAShapeLayer with CGPath of your shape.
Rounded rectangle path can be constructed using Uibezierpath convenience method.
You can set a line pattern for the shape layer. Animating the line property of the shape layer would give the "marching ants like effect".
shapeLayer = [CAShapeLayer layer];
CGRect shapeRect = CGRectMake(0.0f, 0.0f, 200.0f, 100.0f);
[shapeLayer setBounds:shapeRect];
[shapeLayer setPosition:CGPointMake(160.0f, 140.0f)];
[shapeLayer setFillColor:[[UIColor clearColor] CGColor]];
[shapeLayer setStrokeColor:[[UIColor blackColor] CGColor]];
[shapeLayer setLineWidth:1.0f];
[shapeLayer setLineJoin:kCALineJoinRound];
[shapeLayer setLineDashPattern:
[NSArray arrayWithObjects:[NSNumber numberWithInt:10],
[NSNumber numberWithInt:5],
nil]];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:shapeRect cornerRadius:15.0];
[shapeLayer setPath:path.CGPath];
[[imageview layer] addSublayer:shapeLayer];
The animation function can be,
- (void)toggleMarching
{
if ([shapeLayer animationForKey:#"linePhase"])
[shapeLayer removeAnimationForKey:#"linePhase"];
else {
CABasicAnimation *dashAnimation;
dashAnimation = [CABasicAnimation
animationWithKeyPath:#"lineDashPhase"];
[dashAnimation setFromValue:[NSNumber numberWithFloat:0.0f]];
[dashAnimation setToValue:[NSNumber numberWithFloat:15.0f]];
[dashAnimation setDuration:0.75f];
[dashAnimation setRepeatCount:10000];
[shapeLayer addAnimation:dashAnimation forKey:#"linePhase"];
}
}

Resources