15

I want to draw a very thin hairline width of a line in my UIView's drawRect method. The line I see that has a value 0.5 for CGContextSetLineWidth doesn't match the same 1.0 width value that is used to draw a border CALayer.

You can see the difference between the two - the red line (width = 1) is a lot thinner than the purple/blue line (width = 0.5).

Border drawn with CALayer

Here's how I am drawing my pseudo 1.0 width horizontal line:

CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextSetLineWidth(ctx, 0.5); // I expected a very thin line

CGContextMoveToPoint(ctx, 0, y);
CGContextAddLineToPoint(ctx, self.bounds.size.width, y);

CGContextStrokePath(ctx);

Here's a border for the same view, this time using 1.0 border width:

UIView *myView = (UIView *)self;
CALayer *layer = myView.layer;
layer.borderColor = [UIColor redColor].CGColor;
layer.borderWidth = 1.0;

What do I need to do differently to draw my own custom line that's the same width as the CALayer version?

Howard Spear
  • 551
  • 1
  • 5
  • 14

2 Answers2

38

When you stroke a path, the stroke straddles the path. To say it another way, the path lies along the center of the stroke.

If the path runs along the edge between two pixels, then the stroke will (partially) cover the pixels on both sides of that edge. With a line width of 0.5, a horizontal stroke will extend 0.25 points into the pixel above the path, and 0.25 points into the pixel below the path.

You need to move your path so it doesn't run along the edge of the pixels:

CGFloat lineWidth = 0.5f;
CGContextSetLineWidth(ctx, lineWidth);

// Move the path down by half of the line width so it doesn't straddle pixels.
CGContextMoveToPoint(ctx, 0, y + lineWidth * 0.5f);
CGContextAddLineToPoint(ctx, self.bounds.size.width, y + lineWidth * 0.5f);

But since you're just drawing a horizontal line, it's simpler to use CGContextFillRect:

CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextFillRect(ctx, CGRectMake(0, y, self.bounds.size.width, 0.5f));
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • It's a kind of magic! It works, but could you explain why i should move the path down by half of the line width or give some link? – PSsam Nov 29 '13 at 08:37
  • But he just explained so clearly, in 6 clear lines of text… Avoid straddling the pixel boundaries when drawing thin lines. The Antialiasing of the drawing mechanism create "smeared" line which visually looks thicker than what you expected, but they're not. – Motti Shneor Mar 25 '14 at 20:20
16

You need to turn off antialiasing to get a thin line when not drawn on an integral.

CGContextSetShouldAntialias(ctx, NO)
Conor
  • 1,781
  • 17
  • 27
  • Glad to be of help, I have often overlooked antialias myself. – Conor Jul 16 '13 at 08:33
  • I was working this on an iOS7 app, 1 pixel line in retina, this command had no affect. YMMV. – David H Sep 28 '13 at 01:28
  • this is better than moving the path so that it doesn't straddle pixels. thanks. – Ben H Jan 24 '14 at 21:47
  • I'd manually move away from the pixel boundary, and avoid shutting off antiAliasing, because most drawing in the view looks much better when antiAliasing is on All diagonal lines and curves will look jaggy after calling the above command. – Motti Shneor Mar 25 '14 at 20:24
  • You only turn off antialias for your own drawing code. It goes without saying that the drawing context should be saved and popped for good coding habits. – Conor Mar 26 '14 at 06:55