13

I was wondering what is the best way to draw a single point line? My goal is to draw this line in a tableViewCell to make it look just like the native cell separator. I don't want to use the native separator because i want to make in a different color and in a different position (not the bottom..).

At first i was using a 1px UIView and colored it in grey. But in Retina displays it looks like 2px. Also tried using this method:

- (void)drawLine:(CGPoint)startPoint endPoint:(CGPoint)endPoint inColor:(UIColor *)color {

    CGMutablePathRef straightLinePath = CGPathCreateMutable();
    CGPathMoveToPoint(straightLinePath, NULL, startPoint.x, startPoint.y);
    CGPathAddLineToPoint(straightLinePath, NULL, endPoint.x, endPoint.y);

    CAShapeLayer *shapeLayer = [CAShapeLayer layer];
    shapeLayer.path = straightLinePath;
    UIColor *fillColor = color;
    shapeLayer.fillColor = fillColor.CGColor;
    UIColor *strokeColor = color;
    shapeLayer.strokeColor = strokeColor.CGColor;
    shapeLayer.lineWidth = 0.5f;
    shapeLayer.fillRule = kCAFillRuleNonZero;
    [self.layer addSublayer:shapeLayer];
}

It works in like 60% of the times for some reason.. Is something wrong with it? Anyway ,i'd be happy to hear about a better way.

Thanks.

Lirik
  • 3,167
  • 1
  • 30
  • 31
  • Have you tried changing the alpha in a 1px UIView ? it will look like it's thinner. – streem Mar 27 '14 at 16:29
  • I'm looking for a more generic way , and anyway i need a solid color for it :/ – Lirik Mar 27 '14 at 17:35
  • 1
    Although this was a great QA - **here's a much more modern solution** http://stackoverflow.com/a/34766567/294884 – Fattie Jul 01 '16 at 22:33

5 Answers5

17

I did the same with a UIView category. Here are my methods :

#define SEPARATOR_HEIGHT 0.5

- (void)addSeparatorLinesWithColor:(UIColor *)color
{
    [self addSeparatorLinesWithColor:color edgeInset:UIEdgeInsetsZero];
}


- (void)addSeparatorLinesWithColor:(UIColor *)color edgeInset:(UIEdgeInsets)edgeInset
{

    UIView *topSeparatorView = [[UIView alloc] initWithFrame:CGRectMake(edgeInset.left, - SEPARATOR_HEIGHT, self.frame.size.width - edgeInset.left - edgeInset.right, SEPARATOR_HEIGHT)];
    [topSeparatorView setBackgroundColor:color];
    [self addSubview:topSeparatorView];

    UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake(edgeInset.left, self.frame.size.height + SEPARATOR_HEIGHT, self.frame.size.width - edgeInset.left - edgeInset.right, SEPARATOR_HEIGHT)];
    [separatorView setBackgroundColor:color];
    [self addSubview:separatorView];
}

Just to add to Rémy's great answer, it's perhaps even simpler to do this. Make a class UILine.m

@interface UILine:UIView
@end
@implementation UILine
-(id)awakeFromNib
    {
    // careful, contentScaleFactor does NOT WORK in storyboard during initWithCoder.
    // example, float sortaPixel = 1.0/self.contentScaleFactor ... does not work.
    // instead, use mainScreen scale which works perfectly:
    float sortaPixel = 1.0/[UIScreen mainScreen].scale;
    UIView *topSeparatorView = [[UIView alloc] initWithFrame:
        CGRectMake(0, 0, self.frame.size.width, sortaPixel)];

    topSeparatorView.userInteractionEnabled = NO;
    [topSeparatorView setBackgroundColor:self.backgroundColor];
    [self addSubview:topSeparatorView];

    self.backgroundColor = [UIColor clearColor];
    self.userInteractionEnabled = NO;
    }
@end

In IB, drop in a UIView, click identity inspector and rename the class to a UILine. Set the width you want in IB. Set the height to 1 or 2 pixels - simply so you can see it in IB. Set the background colour you want in IB. When you run the app it will become a 1-pixel line, that width, in that colour. (You probably should not be affected by any default autoresize settings in storyboard/xib, I couldn't make it break.) You're done.

Note: you may think "Why not just resize the UIView in code in awakeFromNib?" Resizing views upon loading, in a storyboard app, is problematic - see the many questions here about it!

Interesting gotchya: it's likely you'll just make the UIView, say, 10 or 20 pixels high on the storyboard, simply so you can see it. Of course it disappears in the app and you get the pretty one pixel line. But! be sure to remember self.userInteractionEnabled = NO, or it might get over your other, say, buttons!


2016 solution ! https://stackoverflow.com/a/34766567/294884

Community
  • 1
  • 1
Rémy Virin
  • 3,379
  • 23
  • 42
  • this is not what we call `draw` – Laszlo Mar 27 '14 at 16:50
  • 1
    This is exactly drawing. ascii art is drawing. You would often say "I drew the input form using squares, views, UIButtons, drawRect and other techniques". Thanks for the great answer, Rémy! – Fattie Mar 30 '14 at 15:41
  • Ended up creating a UIView category with Joe Blow's version of this solution. It's the simplest solution and it worked great. thanks! – Lirik Apr 08 '14 at 08:15
2
shapeLayer.lineWidth = 0.5f;

That's a common mistake and is the reason this is working only some of the time. Sometimes this will overlap pixels on the screen exactly and sometimes it won't. The way to draw a single-point line that always works is to draw a one-point-thick rectangle on integer boundaries, and fill it. That way, it will always match the pixels on the screen exactly.

To convert from points to pixels, if you want to do that, use the view's scale factor.

Thus, this will always be one pixel wide:

CGContextFillRect(con, CGRectMake(0,0,desiredLength,1.0/self.contentScaleFactor));

Here's a screen shot showing the line used as a separator, drawn at the top of each cell:

enter image description here

The table view itself has no separators (as is shown by the white space below the three existing cells). I may not be drawing the line in the position, length, and color that you want, but that's your concern, not mine.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Ok, i tried these lines of code (called form drawRect: method) - and nothing is drawn, can see the problem?: CGContextRef context = UIGraphicsGetCurrentContext(); CGContextFillRect(context, CGRectMake(startPoint.x, startPoint.y, width, 1.0/self.contentScaleFactor)); – Lirik Mar 28 '14 at 08:33
  • @matt, do you happen to know: if one uses the "UIView height" trick Rémy mentions below. (Probably using 1.0/self.contentScaleFactor rather than just "1/2" - unless you're exclusively for retina I guess.) In fact. Will that suffer the **non-integer boundary problem**?? I couldn't "make it exhibit" the non-integer boundary, but I may not have tried hard enough :) I guess, if you drop in a UIView from interface builder, it will indeed always be on an integer boundary (I think). thanks if you know, cheers – Fattie Mar 30 '14 at 16:05
  • 1
    @JoeBlow Exactly right - it's all about integer boundaries. Views should _always_ be on integer boundaries. If you do it in IB, IB enforces this. If you add a view in code, you should always call CGRectIntegral on its frame if you're in any doubt. The main place to go wrong here is if you position a view by its _center_: if the view has an odd dimension, its frame is now on a non-integer boundary. Avoid that. – matt Mar 30 '14 at 16:25
  • @matt - brilliant. So in short if your view is only ever positioned by IB, it will be integer-wise. If you move it around by code, it could indeed be off-integer. Thanks again very much. – Fattie Mar 30 '14 at 16:26
2

AutoLayout method:

I use a plain old UIView and set its height constraint to 1 in Interface Builder. Attached it to the bottom via constraints. Interface builder doesn't allow you to set the height constraint to 0.5, but you can do it in code.

Make a connector for the height constraint, then call this:

// Note: This will be 0.5 on retina screens
self.dividerViewHeightConstraint.constant =  1.0/[UIScreen mainScreen].scale 

Worked for me.

FWIW I don't think we need to support non-retina screens anymore. However, I am still using the main screen scale to future proof the app.

n13
  • 6,843
  • 53
  • 40
1

You have to take into account the scaling due to retina and that you are not referring to on screen pixels. See Core Graphics Points vs. Pixels.

Volker
  • 4,640
  • 1
  • 23
  • 31
1

Addition to Rémy Virin's answer, using Swift 3.0 Creating LineSeparator class:

import UIKit

class LineSeparator: UIView {

override func awakeFromNib() {

    let sortaPixel: CGFloat = 1.0/UIScreen.main.scale

    let topSeparatorView = UIView()
    topSeparatorView.frame = CGRect(x: 0, y: 0, width: self.frame.size.width, height: sortaPixel)

    topSeparatorView.isUserInteractionEnabled = false
    topSeparatorView.backgroundColor = self.backgroundColor

    self.addSubview(topSeparatorView)
    self.backgroundColor = UIColor.clear

    self.isUserInteractionEnabled = false   
    }
}
Mr_Vlasov
  • 515
  • 2
  • 6
  • 25