Setting CAShapeLayer lineWidth Less Than 1 - ios

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

Related

UIView Draw Circle with Dotted Line Border

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

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;

How to draw a smooth circle with CAShapeLayer and UIBezierPath?

I am attempting to draw a stroked circle by using a CAShapeLayer and setting a circular path on it. However, this method is consistently less accurate when rendered to the screen than using borderRadius or drawing the path in a CGContextRef directly.
Here are the results of all three methods:
Notice that the third is poorly rendered, especially inside the stroke on the top and bottom.
I have set the contentsScale property to [UIScreen mainScreen].scale.
Here is my drawing code for these three circles. What’s missing to make the CAShapeLayer draw smoothly?
#interface BCViewController ()
#end
#interface BCDrawingView : UIView
#end
#implementation BCDrawingView
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
self.backgroundColor = nil;
self.opaque = YES;
}
return self;
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
[[UIColor whiteColor] setFill];
CGContextFillRect(UIGraphicsGetCurrentContext(), rect);
CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), NULL);
[[UIColor redColor] setStroke];
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 1);
[[UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)] stroke];
}
#end
#interface BCShapeView : UIView
#end
#implementation BCShapeView
+ (Class)layerClass
{
return [CAShapeLayer class];
}
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
self.backgroundColor = nil;
CAShapeLayer *layer = (id)self.layer;
layer.lineWidth = 1;
layer.fillColor = NULL;
layer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)].CGPath;
layer.strokeColor = [UIColor redColor].CGColor;
layer.contentsScale = [UIScreen mainScreen].scale;
layer.shouldRasterize = NO;
}
return self;
}
#end
#implementation BCViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UIView *borderView = [[UIView alloc] initWithFrame:CGRectMake(24, 104, 36, 36)];
borderView.layer.borderColor = [UIColor redColor].CGColor;
borderView.layer.borderWidth = 1;
borderView.layer.cornerRadius = 18;
[self.view addSubview:borderView];
BCDrawingView *drawingView = [[BCDrawingView alloc] initWithFrame:CGRectMake(20, 40, 44, 44)];
[self.view addSubview:drawingView];
BCShapeView *shapeView = [[BCShapeView alloc] initWithFrame:CGRectMake(20, 160, 44, 44)];
[self.view addSubview:shapeView];
UILabel *borderLabel = [UILabel new];
borderLabel.text = #"CALayer borderRadius";
[borderLabel sizeToFit];
borderLabel.center = CGPointMake(borderView.center.x + 26 + borderLabel.bounds.size.width/2.0, borderView.center.y);
[self.view addSubview:borderLabel];
UILabel *drawingLabel = [UILabel new];
drawingLabel.text = #"drawRect: UIBezierPath";
[drawingLabel sizeToFit];
drawingLabel.center = CGPointMake(drawingView.center.x + 26 + drawingLabel.bounds.size.width/2.0, drawingView.center.y);
[self.view addSubview:drawingLabel];
UILabel *shapeLabel = [UILabel new];
shapeLabel.text = #"CAShapeLayer UIBezierPath";
[shapeLabel sizeToFit];
shapeLabel.center = CGPointMake(shapeView.center.x + 26 + shapeLabel.bounds.size.width/2.0, shapeView.center.y);
[self.view addSubview:shapeLabel];
}
#end
EDIT: For those who cannot see the difference, I've drawn circles on top of each other and zoomed in:
Here I've drawn a red circle with drawRect:, and then drawn an identical circle with drawRect: again in green on top of it. Note the limited bleed of red. Both of these circles are "smooth" (and identical to the cornerRadius implementation):
In this second example, you'll see the issue. I've drawn once using a CAShapeLayer in red, and again on top with a drawRect: implementation of the same path, but in green. Note that you can see a lot more inconsistency with more bleed from the red circle underneath. It's clearly being drawn in a different (and worse) fashion.
Who knew there are so many ways to draw a circle?
TL;DR: If you want to use CAShapeLayer and still get smooth circles, you'll need to use shouldRasterize and rasterizationScale carefully.
Original
Here's your original CAShapeLayer and a diff from the drawRect version. I made a screenshot off my iPad Mini with Retina Display, then massaged it in Photoshop, and blew it up to 200%. As you can clearly see, the CAShapeLayer version has visible differences, especially on the left and right edges (darkest pixels in the diff).
Rasterize at screen scale
Let's rasterize at screen scale, which should be 2.0 on retina devices. Add this code:
layer.rasterizationScale = [UIScreen mainScreen].scale;
layer.shouldRasterize = YES;
Note that rasterizationScale defaults to 1.0 even on retina devices, which accounts for the fuzziness of default shouldRasterize.
The circle is now a little smoother, but the bad bits (darkest pixels in the diff) have moved to the top and bottom edges. Not appreciably better than no rasterizing!
Rasterize at 2x screen scale
layer.rasterizationScale = 2.0 * [UIScreen mainScreen].scale;
layer.shouldRasterize = YES;
This rasterizes the path at 2x screen scale, or up to 4.0 on retina devices.
The circle is now visibly smoother, the diffs are much lighter and spread out evenly.
I also ran this in Instruments: Core Animation and didn't see any major differences in the Core Animation Debug Options. However it may be slower since it's downscaling not just blitting an offscreen bitmap to the screen. You may also need to temporarily set shouldRasterize = NO while animating.
What doesn't work
Set shouldRasterize = YES by itself. On retina devices, this looks fuzzy because rasterizationScale != screenScale.
Set contentScale = screenScale. Since CAShapeLayer doesn't draw into contents, whether or not it is rasterizing, this doesn't affect the rendition.
Credit to Jay Hollywood of Humaan, a sharp graphic designer who first pointed it out to me.
Ah, i ran into the same problem some time ago (it was still iOS 5 then iirc), and I wrote the following comment in the code:
/*
ShapeLayer
----------
Fixed equivalent of CAShapeLayer.
CAShapeLayer is meant for animatable bezierpath
and also doesn't cache properly for retina display.
ShapeLayer converts its path into a pixelimage,
honoring any displayscaling required for retina.
*/
A filled circle underneath a circleshape would bleed its fillcolor. Depending on the colors this would be very noticeable. And during userinteraction the shape would render even worse, which let me to conclude that the shapelayer would always render with a scalefactor of 1.0, regardless of the layer scalefactor, because it is meant for animation purposes.
i.e. you only use a CAShapeLayer if you have a specific need for animatable changes to the shape of the bezierpath, not to any of the other properties that are animatable through the usual layer properties.
I eventually decided to write a simple ShapeLayer that would cache its own result, but you might try implementing the displayLayer: or the drawLayer:inContext:
Something like:
- (void)displayLayer:(CALayer *)layer
{
UIImage *image = nil;
CGContextRef context = UIImageContextBegin(layer.bounds.size, NO, 0.0);
if (context != nil)
{
[layer renderInContext:context];
image = UIImageContextEnd();
}
layer.contents = image;
}
I haven't tried that, but would be interesting to know the result...
I know this is an older question, but for those who are trying to work in the drawRect method and still having trouble, one small tweak that helped me immensely was using the correct method to fetch the UIGraphicsContext. Using the default:
let newSize = CGSize(width: 50, height: 50)
UIGraphicsBeginImageContext(newSize)
let context = UIGraphicsGetCurrentContext()
would result in blurry circles no matter which suggestion I followed from the other answers. What finally did it for me was realizing that the default method for getting an ImageContext sets the scaling to non-retina. To get an ImageContext for a retina display you need to use this method:
let newSize = CGSize(width: 50, height: 50)
UIGraphicsBeginImageContextWithOptions(newSize, false, 0)
let context = UIGraphicsGetCurrentContext()
from there using the normal drawing methods worked fine. Setting the last option to 0 will tell the system to use the scaling factor of the device’s main screen. The middle option false is used to tell the graphics context whether or not you'll be drawing an opaque image (true means the image will be opaque) or one that needs an alpha channel included for transparencies. Here are the appropriate Apple Docs for more context: https://developer.apple.com/reference/uikit/1623912-uigraphicsbeginimagecontextwitho?language=objc
I guess CAShapeLayer is backed by a more performant way of rendering its shapes and takes some shortcuts. Anyway CAShapeLayer can be a little bit slow on the main thread. Unless you need to animate between different paths I would suggest render asynchronously to a UIImage on a background thread.
Use this method to draw UIBezierPath
/*********draw circle where double tapped******/
- (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;
}
And draw like this
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [[self makeCircleAtLocation:location radius:50.0] CGPath];
shapeLayer.strokeColor = [[UIColor whiteColor] CGColor];
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 3.0;
// Add CAShapeLayer to our view
[gesture.view.layer addSublayer:shapeLayer];

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!

UILabel : How can I change all the borders except the bottom one?

I know I can change a border's object with
item.layer.cornerRadius = floatValue;
item.layer.borderWidth = intValue;
item.layer.borderColor = colorValue;
But how can I only change top, left and right borders ?
Thank you for your advices.
I don't think you can do that directly.
There are a couple of responses to this question that might help, including one that links to some open source code that solves the problem.
You could use another layer to mask away the corners that you don't want to see. This has the downside that you:
can't have a shadow
can't have another mask (if you don't do them together)
will loose half the border width since the border is stroked on the center of your border
If that is okay with you, here is a sample code that should get you started
CGFloat borderWidth = 4.0;
[[myView layer] setBorderWidth:borderWidth];
CALayer *mask = [CALayer layer];
// The mask needs to be filled to mask
[mask setBackgroundColor:[[UIColor blackColor] CGColor]];
// Make the masks frame smaller in height
CGRect maskFrame = CGRectInset([myView bounds], 0, borderWidth);
// Move the maskFrame to the top
maskFrame.origin.y = 0;
[mask setFrame:maskFrame];
[[myView layer] setMask:mask];

Resources